Skip to content

Commit 8be0580

Browse files
authored
Cherrypick v0.29.7 (#953)
* Skip global widget window from available screen sharing sources (#930) * Fix panic in handleBotGetProfileForSession (#942) * [MM-62241] Fix screen sharing from popout on new Desktop versions (#948) * Fix screen sharing from popout on new Desktop versions * Fix linting * Fix leaving call from call post in main desktop view * Bump transcriber and recorder images
1 parent 4023963 commit 8be0580

File tree

12 files changed

+275
-18
lines changed

12 files changed

+275
-18
lines changed

.github/workflows/e2e.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ jobs:
102102
DOCKER_NETWORK: playwright_tests
103103
CONTAINER_SERVER: playwright_tests_server
104104
IMAGE_CALLS_OFFLOADER: mattermost/calls-offloader:v0.8.0
105-
IMAGE_CALLS_RECORDER: mattermost/calls-recorder:v0.7.5
105+
IMAGE_CALLS_RECORDER: mattermost/calls-recorder:v0.8.0
106106
IMAGE_SERVER: mattermostdevelopment/mattermost-enterprise-edition:master
107107
IMAGE_CURL: curlimages/curl:8.7.1
108108
CI_NODE_INDEX: ${{ matrix.run_id }}

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ e2e/plugin.json
3939

4040
# server bundled translations
4141
server/assets/i18n
42+
.aider*

e2e/tests/desktop.spec.ts

+37
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ test.beforeEach(async ({page}, info) => {
4646
desktopAPICalls.leaveCall = true;
4747
});
4848

49+
await page.exposeFunction('getDesktopSources', () => {
50+
// The base64 image string needs to be in this scope since this function executed on the page.
51+
const thumbnailURL = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAADhCAAAAADD3bzAAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQfmARELKziLK+FjAAACGUlEQVR42u3RQQ0AIBDAMMC/rBPGGwXs0SpYsj2LkvM7gJchMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJjSIwhMYbEGBJzARuNAobpTuE8AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIyLTAxLTE3VDExOjQzOjU2KzAwOjAwvzbXMAAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMi0wMS0xN1QxMTo0Mzo1NiswMDowMM5rb4wAAAAASUVORK5CYII=';
52+
return [
53+
{id: '1', name: 'source name 1', thumbnailURL},
54+
{id: '2', name: 'source name 2', thumbnailURL},
55+
{id: '3', name: 'Calls Widget', thumbnailURL},
56+
];
57+
});
58+
4959
await page.addInitScript(() => {
5060
window.desktopAPI = {
5161
getAppInfo: window.getAppInfo,
@@ -62,6 +72,7 @@ test.beforeEach(async ({page}, info) => {
6272
},
6373
sendCallsError: window.sendCallsError,
6474
leaveCall: window.leaveCall,
75+
getDesktopSources: window.getDesktopSources,
6576
};
6677
});
6778
}
@@ -180,6 +191,32 @@ test.describe('desktop', () => {
180191
await devPage.leaveCall();
181192
});
182193

194+
test('desktopAPI: widget window should be excluded from sharing sources', async ({page}) => {
195+
const devPage = new PlaywrightDevPage(page);
196+
await devPage.startCall();
197+
198+
await page.evaluate(() => {
199+
window.desktop = {version: '5.10.0'};
200+
});
201+
202+
await page.locator('#calls-widget-toggle-menu-button').click();
203+
await page.locator('#calls-widget-menu-screenshare').click();
204+
await expect(page.locator('#calls-screen-source-modal')).toBeVisible();
205+
expect(await page.locator('#calls-screen-source-modal').screenshot()).toMatchSnapshot('calls-screen-source-modal-no-widget.png');
206+
207+
// Verify widget window is not in the list of sources
208+
await expect(page.getByText('source name 1')).toBeVisible();
209+
await expect(page.getByText('source name 2')).toBeVisible();
210+
await expect(page.getByText('Calls Widget')).not.toBeVisible();
211+
212+
await page.locator('#calls-screen-source-modal button:has-text("source name 2")').click();
213+
214+
await page.locator('#calls-screen-source-modal button:has-text("Share")').click();
215+
await expect(page.locator('#calls-screen-source-modal')).toBeHidden();
216+
217+
await devPage.leaveCall();
218+
});
219+
183220
test('desktopAPI: screen sharing permissions error', async ({page}) => {
184221
await page.addInitScript(() => {
185222
window.desktopAPI.onScreenShared = (listener: (sourceID: string, withAudio: boolean) => void) => {
Loading
Loading

e2e/types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,16 @@ declare global {
1818
onScreenShared: () => void,
1919
sendCallsError: () => void,
2020
leaveCall: () => void,
21+
getDesktopSources: () => DesktopSource[],
2122
}
2223
}
2324

25+
export type DesktopSource = {
26+
id: string;
27+
name: string;
28+
thumbnailURL: string;
29+
};
30+
2431
export type UserState = {
2532
username: string;
2633
password: string;

plugin.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@
325325
"props": {
326326
"min_rtcd_version": "v0.12.0",
327327
"min_offloader_version": "v0.8.0",
328-
"calls_recorder_version": "v0.7.3",
329-
"calls_transcriber_version": "v0.3.1"
328+
"calls_recorder_version": "v0.8.0",
329+
"calls_transcriber_version": "v0.6.0"
330330
}
331331
}

server/bot_api.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -596,8 +596,8 @@ func (p *Plugin) handleBotGetProfileForSession(w http.ResponseWriter, r *http.Re
596596
return
597597
}
598598

599-
ust := state.sessions[sessionID]
600-
if ust.UserID == "" {
599+
ust, ok := state.sessions[sessionID]
600+
if !ok {
601601
res.Code = http.StatusNotFound
602602
res.Err = "not found"
603603
return

server/bot_api_test.go

+218
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
// Copyright (c) 2022-present Mattermost, Inc. All Rights Reserved.
2+
// See LICENSE.txt for license information.
3+
4+
package main
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"net/http"
10+
"net/http/httptest"
11+
"testing"
12+
13+
"github.com/mattermost/mattermost-plugin-calls/server/cluster"
14+
"github.com/mattermost/mattermost-plugin-calls/server/enterprise"
15+
"github.com/mattermost/mattermost-plugin-calls/server/public"
16+
"github.com/mattermost/mattermost/server/public/model"
17+
"github.com/mattermost/mattermost/server/public/plugin"
18+
19+
serverMocks "github.com/mattermost/mattermost-plugin-calls/server/mocks/github.com/mattermost/mattermost-plugin-calls/server/interfaces"
20+
pluginMocks "github.com/mattermost/mattermost-plugin-calls/server/mocks/github.com/mattermost/mattermost/server/public/plugin"
21+
22+
"github.com/stretchr/testify/mock"
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
func TestHandleBotGetProfileForSession(t *testing.T) {
27+
mockAPI := &pluginMocks.MockAPI{}
28+
mockMetrics := &serverMocks.MockMetrics{}
29+
30+
botUserID := model.NewId()
31+
32+
p := Plugin{
33+
MattermostPlugin: plugin.MattermostPlugin{
34+
API: mockAPI,
35+
},
36+
metrics: mockMetrics,
37+
botSession: &model.Session{
38+
UserId: botUserID,
39+
},
40+
callsClusterLocks: map[string]*cluster.Mutex{},
41+
}
42+
43+
p.licenseChecker = enterprise.NewLicenseChecker(p.API)
44+
45+
store, tearDown := NewTestStore(t)
46+
t.Cleanup(tearDown)
47+
p.store = store
48+
49+
mockAPI.On("KVSetWithOptions", mock.Anything, mock.Anything, mock.Anything).Return(true, nil)
50+
mockMetrics.On("ObserveClusterMutexGrabTime", "mutex_call", mock.AnythingOfType("float64"))
51+
mockMetrics.On("ObserveAppHandlersTime", mock.AnythingOfType("string"), mock.AnythingOfType("float64"))
52+
mockMetrics.On("ObserveClusterMutexLockedTime", "mutex_call", mock.AnythingOfType("float64"))
53+
mockMetrics.On("Handler").Return(nil).Once()
54+
55+
mockAPI.On("GetConfig").Return(&model.Config{}, nil)
56+
mockAPI.On("GetLicense").Return(&model.License{
57+
SkuShortName: "enterprise",
58+
}, nil)
59+
60+
apiRouter := p.newAPIRouter()
61+
62+
t.Run("no call ongoing", func(t *testing.T) {
63+
defer mockAPI.AssertExpectations(t)
64+
defer mockMetrics.AssertExpectations(t)
65+
66+
channelID := model.NewId()
67+
sessionID := model.NewId()
68+
69+
mockAPI.On("LogDebug", "creating cluster mutex for call",
70+
"origin", mock.AnythingOfType("string"), "channelID", channelID).Once()
71+
mockAPI.On("KVDelete", "mutex_call_"+channelID).Return(nil).Once()
72+
73+
w := httptest.NewRecorder()
74+
r := httptest.NewRequest("GET", fmt.Sprintf("/bot/calls/%s/sessions/%s/profile", channelID, sessionID), nil)
75+
r.Header.Set("Mattermost-User-Id", botUserID)
76+
77+
apiRouter.ServeHTTP(w, r)
78+
79+
resp := w.Result()
80+
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
81+
var res httpResponse
82+
err := json.NewDecoder(resp.Body).Decode(&res)
83+
require.NoError(t, err)
84+
require.Equal(t, "no call ongoing", res.Msg)
85+
require.Equal(t, 400, res.Code)
86+
})
87+
88+
t.Run("session not found", func(t *testing.T) {
89+
defer mockAPI.AssertExpectations(t)
90+
defer mockMetrics.AssertExpectations(t)
91+
92+
channelID := model.NewId()
93+
sessionID := model.NewId()
94+
call := &public.Call{
95+
ID: model.NewId(),
96+
ChannelID: channelID,
97+
CreateAt: 45,
98+
StartAt: 45,
99+
OwnerID: botUserID,
100+
}
101+
err := store.CreateCall(call)
102+
require.NoError(t, err)
103+
104+
w := httptest.NewRecorder()
105+
r := httptest.NewRequest("GET", fmt.Sprintf("/bot/calls/%s/sessions/%s/profile", channelID, sessionID), nil)
106+
107+
mockAPI.On("LogDebug", "creating cluster mutex for call",
108+
"origin", mock.AnythingOfType("string"), "channelID", channelID).Once()
109+
mockAPI.On("KVDelete", "mutex_call_"+channelID).Return(nil).Once()
110+
111+
r.Header.Set("Mattermost-User-Id", botUserID)
112+
apiRouter.ServeHTTP(w, r)
113+
114+
resp := w.Result()
115+
require.Equal(t, http.StatusNotFound, resp.StatusCode)
116+
var res httpResponse
117+
err = json.NewDecoder(resp.Body).Decode(&res)
118+
require.NoError(t, err)
119+
require.Equal(t, "not found", res.Msg)
120+
require.Equal(t, 404, res.Code)
121+
})
122+
123+
t.Run("get user error", func(t *testing.T) {
124+
channelID := model.NewId()
125+
userID := model.NewId()
126+
sessionID := model.NewId()
127+
call := &public.Call{
128+
ID: model.NewId(),
129+
ChannelID: channelID,
130+
CreateAt: 45,
131+
StartAt: 45,
132+
OwnerID: userID,
133+
}
134+
err := store.CreateCall(call)
135+
require.NoError(t, err)
136+
137+
err = store.CreateCallSession(&public.CallSession{
138+
ID: sessionID,
139+
CallID: call.ID,
140+
UserID: userID,
141+
JoinAt: 45,
142+
})
143+
require.NoError(t, err)
144+
145+
mockAPI.On("GetUser", userID).Return(nil, &model.AppError{
146+
Message: "failed to get user",
147+
}).Once()
148+
149+
w := httptest.NewRecorder()
150+
r := httptest.NewRequest("GET", "/bot/calls/"+channelID+"/sessions/"+sessionID+"/profile", nil)
151+
152+
mockAPI.On("LogDebug", "creating cluster mutex for call",
153+
"origin", mock.AnythingOfType("string"), "channelID", channelID).Once()
154+
mockAPI.On("KVDelete", "mutex_call_"+channelID).Return(nil).Once()
155+
156+
r.Header.Set("Mattermost-User-Id", botUserID)
157+
apiRouter.ServeHTTP(w, r)
158+
159+
resp := w.Result()
160+
require.Equal(t, http.StatusInternalServerError, resp.StatusCode)
161+
162+
var res httpResponse
163+
err = json.NewDecoder(resp.Body).Decode(&res)
164+
require.NoError(t, err)
165+
require.Equal(t, "failed to get user", res.Msg)
166+
require.Equal(t, 500, res.Code)
167+
})
168+
169+
t.Run("success", func(t *testing.T) {
170+
channelID := model.NewId()
171+
userID := model.NewId()
172+
sessionID := model.NewId()
173+
call := &public.Call{
174+
ID: model.NewId(),
175+
ChannelID: channelID,
176+
CreateAt: 45,
177+
StartAt: 45,
178+
OwnerID: userID,
179+
}
180+
err := store.CreateCall(call)
181+
require.NoError(t, err)
182+
183+
err = store.CreateCallSession(&public.CallSession{
184+
ID: sessionID,
185+
CallID: call.ID,
186+
UserID: userID,
187+
JoinAt: 45,
188+
})
189+
require.NoError(t, err)
190+
191+
user := &model.User{
192+
Id: userID,
193+
Username: "testuser",
194+
Email: "test@example.com",
195+
}
196+
mockAPI.On("GetUser", userID).Return(user, nil).Once()
197+
198+
w := httptest.NewRecorder()
199+
r := httptest.NewRequest("GET", "/bot/calls/"+channelID+"/sessions/"+sessionID+"/profile", nil)
200+
201+
mockAPI.On("LogDebug", "creating cluster mutex for call",
202+
"origin", mock.AnythingOfType("string"), "channelID", channelID).Once()
203+
mockAPI.On("KVDelete", "mutex_call_"+channelID).Return(nil).Once()
204+
205+
r.Header.Set("Mattermost-User-Id", botUserID)
206+
apiRouter.ServeHTTP(w, r)
207+
208+
resp := w.Result()
209+
require.Equal(t, http.StatusOK, resp.StatusCode)
210+
211+
var respUser model.User
212+
err = json.NewDecoder(resp.Body).Decode(&respUser)
213+
require.NoError(t, err)
214+
require.Equal(t, userID, respUser.Id)
215+
require.Equal(t, user.Username, respUser.Username)
216+
require.Equal(t, user.Email, respUser.Email)
217+
})
218+
}

webapp/src/components/custom_post_types/post_type/component.tsx

+2-5
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import {
2222
callStartedTimestampFn,
2323
getCallPropsFromPost,
2424
getUserDisplayName,
25-
sendDesktopEvent,
26-
shouldRenderDesktopWidget,
2725
toHuman,
2826
untranslatable,
2927
} from 'src/utils';
@@ -74,9 +72,8 @@ const PostType = ({
7472
// NOTE: this also handles the desktop global widget case since the opener window
7573
// will have the client.
7674
callsClient.disconnect();
77-
} else if (shouldRenderDesktopWidget()) {
78-
// DEPRECATED: legacy Desktop API logic (<= 5.6.0)
79-
sendDesktopEvent('calls-leave-call', {callID: post.channel_id});
75+
} else if (window.desktopAPI?.leaveCall) {
76+
window.desktopAPI.leaveCall();
8077
}
8178
};
8279

webapp/src/components/expanded_view/component.tsx

+1-8
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import React from 'react';
1717
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
1818
import {IntlShape} from 'react-intl';
1919
import {RouteComponentProps} from 'react-router-dom';
20-
import {compareSemVer} from 'semver-parser';
2120
import {hostMuteOthers, hostRemove} from 'src/actions';
2221
import {Badge} from 'src/components/badge';
2322
import CallDuration from 'src/components/call_widget/call_duration';
@@ -74,7 +73,6 @@ import {
7473
isDMChannel,
7574
sendDesktopEvent,
7675
setCallsGlobalCSSVars,
77-
shouldRenderDesktopWidget,
7876
untranslatable,
7977
} from 'src/utils';
8078
import styled, {createGlobalStyle, css} from 'styled-components';
@@ -510,14 +508,9 @@ export default class ExpandedView extends React.PureComponent<Props, State> {
510508
});
511509
this.props.trackEvent(Telemetry.Event.UnshareScreen, Telemetry.Source.ExpandedView, {initiator: fromShortcut ? 'shortcut' : 'button'});
512510
} else if (!this.props.screenSharingSession) {
513-
if (window.desktop && compareSemVer(window.desktop.version, '5.1.0') >= 0) {
514-
this.props.showScreenSourceModal();
515-
} else if (window.desktopAPI?.openScreenShareModal) {
511+
if (window.desktopAPI?.openScreenShareModal) {
516512
logDebug('desktopAPI.openScreenShareModal');
517513
window.desktopAPI.openScreenShareModal();
518-
} else if (shouldRenderDesktopWidget()) {
519-
// DEPRECATED: legacy Desktop API logic (<= 5.6.0)
520-
sendDesktopEvent('desktop-sources-modal-request');
521514
} else {
522515
const stream = await getScreenStream('', hasExperimentalFlag());
523516
if (window.opener && stream) {

webapp/src/components/screen_source_modal/component.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ export default class ScreenSourceModal extends React.PureComponent<Props, State>
212212
this.props.hideScreenSourceModal();
213213
return;
214214
}
215+
216+
// Exclude the calls widget window from the list.
217+
sources = sources.filter((source) => source.name !== 'Calls Widget');
218+
215219
this.setState({
216220
sources,
217221
selected: sources[0]?.id || '',

0 commit comments

Comments
 (0)