Compare commits

...

23 Commits

Author SHA1 Message Date
dependabot[bot]
0a728b77a8 chore(ci): bump actions/cache from 4.2.0 to 5.0.4
Bumps [actions/cache](https://github.com/actions/cache) from 4.2.0 to 5.0.4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4.2.0...668228422ae6a00e4ad889ee87cd7109ec5666a7)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.4
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-26 14:17:52 +00:00
Vishal Malyan
768be97fa4 feat(ci) optimize Android SDK build by caching Gradle dependencies 2026-03-26 15:16:35 +01:00
Philip Örnfeldt
d67096ee8e lang(sv): Small change of wording
Signed-off-by: Örnfeldt Philip (66140321) <philip.ornfeldt@forsakringskassan.se>
2026-03-26 06:57:05 -05:00
Kevin Caballero
783527ac53 lang: add missing translation in main es 2026-03-26 06:56:22 -05:00
Дамян Минков
fc289dd5ae fix(tests): Report worker crash when session cleanup times out. (#17211)
* fix(tests): Report worker crash when session cleanup times out.

When the WebDriver session DELETE request times out, the worker exits
with code 1 before the JUnit reporter can flush, leaving a zero-byte
XML file that is invisible to the report generator. The onWorkerEnd
hook now detects this and writes a failure entry so the crash shows
up in the test report.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* squash: Fix lint errors.

* fix(tests): Use error instead of failure for worker crash XML.

The previous fix incorrectly used <failure> (test assertion failed) for
the zero-byte XML fallback. Since the test may have actually passed and
only the session cleanup timed out, use <error> (infrastructure problem)
with a message clarifying the result is unknown.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* squash: Updates message.

* squash: Bumps connection timeout.

* squash: Bumps webdriver.io dependencies.

* squash:Use junitReportBuilder to build the report.

* Also add an allure report when a worker returns non-zero.

* squash: fix dependency version.

* fix(tests): Increase allure report generation timeout and improve error messages.

5 seconds was too tight for CI with a full test suite of allure results.
Also distinguish between timeout and non-zero exit code in the error message
to make failures easier to diagnose.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Boris Grozev <boris@jitsi.org>
2026-03-25 16:03:56 -05:00
Stephan Paternotte
e84877e91f lang: Update Dutch translations 2026-03-25 11:50:24 -05:00
Mihaela Dumitru
b24e615ded fix(ui) adjust svg fills and backgrounds (#17062) 2026-03-25 08:54:37 +02:00
Mihaela Dumitru
5ad6332632 feat(external-api): send full user context (#17147) 2026-03-24 17:43:36 +02:00
Calinteodor
8006bd05c6 Revert "feat(deep-linking): use same action for join in app if scheme url is undefined" 2026-03-24 13:15:11 +02:00
mishra
048791c858 fix(react-native-sdk): Export JitsiMeeting component
- Change main entry point from index.tsx to dist/index.js
- Add types field pointing to dist/index.d.ts
- Create tsconfig.json for TypeScript compilation
- Add build script: tsc -p tsconfig.json
- Update prepare hook to auto-compile on npm publish
- Add files array to control npm package contents
- Add .npmignore to exclude build artifacts

Resolves #16443 where JitsiMeeting could not be imported from @jitsi/react-native-sdk
2026-03-24 12:32:33 +02:00
Nishant kumar
ad82e557e0 * fix(invite): use URLSearchParams for decoding dial-in room name 2026-03-23 15:22:04 -05:00
Jaya Allamsetty
15511f86be chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2137.0.0+084a5a9c...v2140.0.0+fe26afb0
2026-03-23 13:49:36 -04:00
Naman Jain
7699cfdac5 feat(translation): added missing hindi translations 2026-03-20 14:31:16 -04:00
Calin-Teodor
16fcf27fb7 feat(deep-linking): use same action for join in app if scheme url is undefined 2026-03-19 15:51:24 +02:00
Jaya Allamsetty
94243c797c feat(tracks) Adds UI notification when SS is killed by macOS.
* feat(tracks) Adds UI notification when SS is killed by macOS.
2026-03-17 16:13:56 -04:00
damencho
6564ba52a2 feat(tests): Adds filesharing tests. 2026-03-13 14:32:07 -05:00
damencho
a77ac20db9 fix(file-sharing): Disable dragging on prejoin.
Updates docs to reflect implementation.
2026-03-13 14:32:07 -05:00
mishraditi
6907db1127 fix(settings) Refreshes audio input levels when microphone permission is regranted. 2026-03-13 11:28:47 -04:00
mishraditi
78931d4f0d fix(settings) Fixes missing previews after device permissions are granted. 2026-03-13 10:25:22 -04:00
Jaya Allamsetty
b19d76fbdf chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2135.0.0+17e2281c...v2137.0.0+084a5a9c
2026-03-12 16:54:14 -04:00
Nishant kumar
0676ca5e62 fix(invite): Use decoded room name for dial-in info page url
* fix(invite): normalize room before building dial-in URL to avoid double encoding

* refactor(invite): use getNormalizedRoomName for room normalization
2026-03-12 13:05:47 -05:00
mishraditi
f387ae2113 fix i18n key for breakout room participant ellipsis tooltip 2026-03-12 11:54:20 -05:00
Hristo Terezov
f4a6036c1b fix(custom-panel): Button group
Since we render the button last when it has group 2 out of 4 React prints console.error.
2026-03-11 15:15:45 -05:00
36 changed files with 2096 additions and 2121 deletions

View File

@@ -97,7 +97,7 @@ jobs:
bundler-cache: true
- run: npx react-native info
- name: Cache CocoaPods
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7
with:
path: |
ios/Pods
@@ -135,6 +135,13 @@ jobs:
node -v
npm -v
- run: npm install
- name: Cache Gradle dependencies
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7
with:
path: /root/.gradle
key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle*', 'android/gradle.properties', 'android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- run: |
cd android
./gradlew :sdk:clean
@@ -174,7 +181,7 @@ jobs:
ruby-version: '3.4'
bundler-cache: true
- name: Cache CocoaPods
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 #v4.2.0
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 #v5.0.4
with:
path: |
ios/Pods

View File

@@ -26,7 +26,7 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />

View File

@@ -883,6 +883,7 @@
"or": "o",
"premeeting": "Pre-reunión",
"proceedAnyway": "Continuar de todos modos",
"recordingWarning": "Otros participantes pueden estar grabando esta llamada",
"screenSharingError": "Error al compartir pantalla:",
"startWithPhone": "Iniciar con audio de llamada telefónica",
"unsafeRoomConsent": "Comprendo los riesgos, quiero unirme a la reunión",

File diff suppressed because it is too large Load Diff

View File

@@ -469,6 +469,8 @@
"screenSharingFailed": "Oeps! Er is iets misgegaan, de schermdeling kon niet worden gestart!",
"screenSharingFailedTitle": "Schermdeling mislukt!",
"screenSharingPermissionDeniedError": "Oeps! Er is iets misgegaan met uw toegangsrechten voor schermdeling. Herlaad en probeer opnieuw.",
"screenshareStoppedDiskSpace": "Dit gebeurt als u de zwevende werkbalk van macOS hebt gebruikt om het delen van schermen te stoppen. Het kan ook te wijten zijn aan een lage schijfruimte.",
"screenshareStoppedTitle": "Scherm delen gestopt via systeem",
"searchInSalesforce": "Zoeken in Salesforce",
"searchResults": "Zoekresultaten({{count}})",
"searchResultsDetailsError": "Er ging iets mis bij het ophalen van eigenaargegevens.",

View File

@@ -817,7 +817,7 @@
"focusFail": "{{component}} inte tillgänglig försöker igen om {{ms}} sek",
"gifsMenu": "GIPHY",
"groupTitle": "Notifieringar",
"hostAskedUnmute": "Värden vill att du ska stänga av ljudet",
"hostAskedUnmute": "Värden vill att du ska starta din mikrofon",
"invalidTenant": "Ogiltig tenant",
"invalidTenantHyphenDescription": "Tenant du använder har ogiltiga tecken (startar eller slutar med '-').",
"invalidTenantLengthDescription": "Tenant du använder är för lång",

View File

@@ -469,6 +469,8 @@
"screenSharingFailed": "Oops! Something went wrong, we weren't able to start screen sharing!",
"screenSharingFailedTitle": "Screen sharing failed!",
"screenSharingPermissionDeniedError": "Oops! Something went wrong with your screen sharing permissions. Please reload and try again.",
"screenshareStoppedDiskSpace": "This happens if you used the macOS's floating toolbar to stop screen sharing. It may also be due to low disk space.",
"screenshareStoppedTitle": "Screen sharing stopped via system",
"searchInSalesforce": "Search in Salesforce",
"searchResults": "Search results({{count}})",
"searchResultsDetailsError": "Something went wrong while retrieving owner data.",

2752
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2135.0.0+17e2281c/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2140.0.0+fe26afb0/lib-jitsi-meet.tgz",
"lodash-es": "4.17.23",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -164,12 +164,12 @@
"@types/w3c-image-capture": "1.0.6",
"@types/w3c-web-hid": "1.0.3",
"@types/zxcvbn": "4.4.1",
"@wdio/allure-reporter": "9.23.2",
"@wdio/cli": "9.23.2",
"@wdio/globals": "9.23.0",
"@wdio/junit-reporter": "9.23.2",
"@wdio/local-runner": "9.23.2",
"@wdio/mocha-framework": "9.23.2",
"@wdio/allure-reporter": "9.27.0",
"@wdio/cli": "9.27.0",
"@wdio/globals": "9.27.0",
"@wdio/junit-reporter": "9.27.0",
"@wdio/local-runner": "9.27.0",
"@wdio/mocha-framework": "9.27.0",
"babel-loader": "9.1.0",
"babel-plugin-optional-require": "0.3.1",
"circular-dependency-plugin": "5.2.0",
@@ -184,6 +184,7 @@
"eslint-plugin-typescript-sort-keys": "3.3.0",
"jetifier": "1.6.4",
"jsonwebtoken": "9.0.2",
"junit-report-builder": "5.1.1",
"patch-package": "6.4.7",
"pretty": "2.0.0",
"process": "0.11.10",
@@ -193,7 +194,7 @@
"ts-loader": "9.4.2",
"typescript": "5.7.2",
"unorm": "1.6.0",
"webdriverio": "9.22.0",
"webdriverio": "9.27.0",
"webpack": "5.105.0",
"webpack-bundle-analyzer": "4.4.2",
"webpack-cli": "5.1.4",

View File

@@ -1 +1,6 @@
*.tgz
tsconfig.json
.npmrc
.git
.gitignore
node_modules

View File

@@ -2,7 +2,8 @@
"name": "@jitsi/react-native-sdk",
"version": "0.0.0",
"description": "React Native SDK for Jitsi Meet.",
"main": "index.tsx",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "Apache-2.0",
"author": "",
"homepage": "https://jitsi.org",
@@ -92,9 +93,27 @@
"@babel/plugin-proposal-optional-chaining": "0.0.0"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"postinstall": "node sdk_instructions.js",
"prepare": "node prepare_sdk.js"
"prepare": "node prepare_sdk.js && npm run build"
},
"files": [
"dist",
"android",
"ios",
"index.tsx",
"jitsi-meet-rnsdk.podspec",
"prepare_sdk.js",
"sdk_instructions.js",
"update_dependencies.js",
"update_sdk_dependencies.js",
"README.md",
"images",
"sounds",
"lang",
"modules",
"react"
],
"bugs": {
"url": "https://github.com/jitsi/jitsi-meet/issues"
},

View File

@@ -0,0 +1,17 @@
{
"extends": "../tsconfig.native.json",
"compilerOptions": {
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"emitDeclarationOnly": false
},
"include": [
"index.tsx"
],
"exclude": [
"node_modules",
"dist"
]
}

View File

@@ -95,12 +95,6 @@ export function commonUserJoinedHandling(
const isPromoted = conference?.getMetadataHandler().getMetadata()?.visitors?.promoted?.[id];
const userIdentity = user.getIdentity()?.user;
// Map identity from JWT context to userContext for external API
const userContext = userIdentity ? {
id: userIdentity.id,
name: userIdentity.name
} : undefined;
// the identity and avatar come from jwt and never change in the presence
dispatch(participantJoined({
avatarURL: userIdentity?.avatar,
@@ -113,7 +107,7 @@ export function commonUserJoinedHandling(
isPromoted,
isReplacing,
sources: user.getSources(),
userContext
userContext: userIdentity
}));
}
}

View File

@@ -619,17 +619,12 @@ function _localParticipantJoined({ getState, dispatch }: IStore, next: Function,
const settings = state['features/base/settings'];
const jwtUser = state['features/base/jwt']?.user;
const userContext = jwtUser ? {
id: jwtUser.id,
name: jwtUser.name
} : undefined;
dispatch(localParticipantJoined({
avatarURL: settings.avatarURL,
email: settings.email,
name: settings.displayName,
id: '',
userContext
userContext: jwtUser
}));
return result;

View File

@@ -45,6 +45,7 @@ export interface IParticipant {
}
export interface IUserContext {
[key: string]: any;
id?: string;
name?: string;
}

View File

@@ -1,10 +1,11 @@
import { createTrackMutedEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IStore } from '../../app/types';
import { showErrorNotification, showNotification } from '../../notifications/actions';
import { showErrorNotification, showNotification, showWarningNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT, NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { getCurrentConference } from '../conference/functions';
import { IJitsiConference } from '../conference/reducer';
import { isMacOS } from '../environment/environment';
import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
import { setAudioMuted, setScreenshareMuted, setVideoMuted } from '../media/actions';
import {
@@ -439,6 +440,12 @@ export function trackAdded(track: any) {
track.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED,
() => {
logger.debug(`Local track stopped: ${track}, removing it from the conference`);
if (mediaType === MEDIA_TYPE.SCREENSHARE && isMacOS()) {
dispatch(showWarningNotification({
descriptionKey: 'dialog.screenshareStoppedDiskSpace',
titleKey: 'dialog.screenshareStoppedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}
dispatch({
type: TRACK_STOPPED,
track: {

View File

@@ -214,7 +214,7 @@ export const colorMap = {
// Visitors
visitorsCountBadge: 'warning02', // Visitors count badge background
visitorsCountText: 'uiBackground', // Visitors count badge text
visitorsCountIcon: 'icon04', // Visitors count icon
visitorsCountIcon: 'iconSvgFill', // Visitors count icon
visitorsQueueBackground: 'ui01', // Visitors queue panel background
visitorsQueueText: 'text01', // Visitors queue text
visitorsArrowBackground: 'ui02', // Visitors arrow container background
@@ -355,7 +355,7 @@ export const colorMap = {
dialInSecondaryText: 'text02', // Dial-in summary secondary text
// Reactions
reactionsMenuBackground: 'ui01', // Reactions menu background
reactionsMenuBackground: '#242528', // Reactions menu background
reactionsMenuBorder: 'ui02', // Reactions menu border
reactionsMenuButtonToggled: 'surface01', // Reactions menu button toggled state background
reactionsMenuBoxShadow1: 'ui09', // Reactions menu box shadow primary
@@ -411,7 +411,7 @@ export const colorMap = {
fileSharingItemBorder: 'ui02', // File sharing item hover/border
// Gifs
gifsBackground: 'ui01', // GIFs panel background
gifsBackground: '#242528', // GIFs panel background
gifsText: 'text01', // GIFs panel text
// Whiteboard

View File

@@ -19,6 +19,7 @@ export interface IBreakoutRoomsState {
rooms: IRooms;
userContextCache: {
[participantId: string]: {
[key: string]: any;
id?: string;
name?: string;
};

View File

@@ -9,6 +9,7 @@ export interface IRoom {
jid: string;
role: string;
userContext?: {
[key: string]: any;
id?: string;
name?: string;
};
@@ -38,6 +39,7 @@ export interface IRoomInfoParticipant {
jid: string;
role: string;
userContext?: {
[key: string]: any;
id?: string;
name?: string;
};

View File

@@ -483,6 +483,7 @@ export default reactReduxConnect(_mapStateToProps)(translate(props => {
const { isOpen: isChatOpen } = useSelector((state: IReduxState) => state['features/chat']);
const isFileUploadEnabled = useSelector(isFileUploadingEnabled);
const isOnPrejoin = useSelector(isPrejoinPageVisible);
const handleDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault();
@@ -500,7 +501,7 @@ export default reactReduxConnect(_mapStateToProps)(translate(props => {
e.preventDefault();
e.stopPropagation();
if (!isFileUploadEnabled) {
if (!isFileUploadEnabled || isOnPrejoin) {
return;
}
@@ -510,7 +511,7 @@ export default reactReduxConnect(_mapStateToProps)(translate(props => {
}
dispatch(setFocusedTab(ChatTabs.FILE_SHARING));
}
}, [ isChatOpen, isDragging, isFileUploadEnabled ]);
}, [ isChatOpen, isDragging, isFileUploadEnabled, isOnPrejoin ]);
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
@@ -528,6 +529,7 @@ export default reactReduxConnect(_mapStateToProps)(translate(props => {
return (
<div
data-testid = 'conference-drag-zone'
onDragEnter = { handleDragEnter }
onDragLeave = { handleDragLeave }
onDragOver = { handleDragOver }

View File

@@ -9,7 +9,7 @@ import { isCustomPanelEnabled } from './functions';
const customPanel = {
key: 'custom-panel',
Content: CustomPanelButton,
group: 2
group: 5
};
/**

View File

@@ -235,6 +235,13 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, {}> {
!== this.props.selectedAudioInputId) {
this._createAudioInputTrack(this.props.selectedAudioInputId);
}
if (!prevProps.hasAudioPermission && this.props.hasAudioPermission) {
this._createAudioInputTrack(this.props.selectedAudioInputId)
?.then(() => {
this.props.dispatch(getAvailableDevices());
});
}
}
/**

View File

@@ -194,6 +194,13 @@ class VideoDeviceSelection extends AbstractDialogTab<IProps, IState> {
!== this.props.selectedVideoInputId) {
this._createVideoInputTrack(this.props.selectedVideoInputId);
}
if (!prevProps.hasVideoPermission && this.props.hasVideoPermission) {
this._createVideoInputTrack(this.props.selectedVideoInputId)
?.then(() => {
this.props.dispatch(getAvailableDevices());
});
}
}
/**

View File

@@ -32,6 +32,7 @@ const useStyles = makeStyles()(theme => {
marginBottom: theme.spacing(2),
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.gifsBackground,
'& div:focus': {
border: '1px solid red !important',
@@ -54,7 +55,7 @@ const useStyles = makeStyles()(theme => {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
color: theme.palette.gifsText,
marginTop: theme.spacing(1)
},
@@ -62,7 +63,8 @@ const useStyles = makeStyles()(theme => {
padding: theme.spacing(3),
width: '100%',
boxSizing: 'border-box',
height: '100%'
height: '100%',
backgroundColor: theme.palette.gifsBackground
},
overflowMenu: {

View File

@@ -4,7 +4,6 @@ import BaseApp from '../../../../base/app/components/BaseApp';
import { isMobileBrowser } from '../../../../base/environment/utils';
import GlobalStyles from '../../../../base/ui/components/GlobalStyles.web';
import JitsiThemeProvider from '../../../../base/ui/components/JitsiThemeProvider.web';
import { parseURLParams } from '../../../../base/util/parseURLParams';
import { DIAL_IN_INFO_PAGE_PATH_NAME } from '../../../constants';
import NoRoomError from '../../dial-in-info-page/NoRoomError.web';
@@ -25,7 +24,8 @@ export default class DialInSummaryApp extends BaseApp<any> {
await super.componentDidMount();
// @ts-ignore
const { room } = parseURLParams(window.location, true, 'search');
const params = new URLSearchParams(window.location.search);
const room = params.get('room') || '';
const { href } = window.location;
const ix = href.indexOf(DIAL_IN_INFO_PAGE_PATH_NAME);
const url = (ix > 0 ? href.substring(0, ix) : href) + room;
@@ -36,7 +36,7 @@ export default class DialInSummaryApp extends BaseApp<any> {
? <DialInSummary
className = 'dial-in-page'
clickableNumbers = { isMobileBrowser() }
room = { decodeURIComponent(room) }
room = { room }
scrollable = { true }
showTitle = { true }
url = { url } />

View File

@@ -17,6 +17,7 @@ import { parseURLParams } from '../base/util/parseURLParams';
import {
StatusCode,
appendURLParam,
getNormalizedRoomName,
parseURIString
} from '../base/util/uri';
import { isVpaasMeeting } from '../jaas/functions';
@@ -729,7 +730,7 @@ export function getDialInfoPageURL(state: IReduxState, roomName?: string) {
const conferenceName = roomName ?? getRoomName(state);
const { locationURL } = state['features/base/connection'];
const { href = '' } = locationURL ?? {};
const room = _decodeRoomURI(conferenceName ?? '');
const room = getNormalizedRoomName(conferenceName) ?? '';
const url = didPageUrl || `${href.substring(0, href.lastIndexOf('/'))}/${DIAL_IN_INFO_PAGE_PATH_NAME}`;

View File

@@ -204,7 +204,7 @@ export const CollapsibleRoom = ({
participantID = { p.jid }>
{!overflowDrawer && moderator && (
<ParticipantActionEllipsis
accessibilityLabel = { t('breakoutRoom.more') }
accessibilityLabel = { t('breakoutRooms.actions.more') }
onClick = { getEllipsisClickHandler(p.jid, p.displayName) } />
)}
</ParticipantItem>

View File

@@ -87,30 +87,6 @@ paths:
$ref: '#/components/schemas/AddDocumentResponse'
example:
fileId: e393a7e5-e790-4f43-836e-d27238201904
delete:
tags:
- Document sharing history
summary: Deletes documents for a given session, user and customer
operationId: deleteDocumentsForSessionCustomerAndUser
parameters:
- name: sessionId
in: path
required: true
schema:
type: string
- name: user-id
in: query
required: true
schema:
type: string
- name: customer-id
in: query
required: true
schema:
type: string
responses:
'204':
description: No Content
/v1/documents/sessions/{sessionId}/files/{fileId}:
get:
tags:
@@ -141,7 +117,7 @@ paths:
tags:
- Document sharing history
summary: Delete a file by sessionId and fileId
description: Delete a file by sessionId and fileId allowed by all moderators
description: Delete a file by sessionId and fileId allowed by the "file-upload" feature
operationId: deleteFile
parameters:
- name: sessionId

View File

@@ -8,6 +8,7 @@ import { IConfig } from '../../react/features/base/config/configType';
import { urlObjectToString } from '../../react/features/base/util/uri';
import BreakoutRooms from '../pageobjects/BreakoutRooms';
import ChatPanel from '../pageobjects/ChatPanel';
import FileSharingPanel from '../pageobjects/FileSharingPanel';
import Filmstrip from '../pageobjects/Filmstrip';
import IframeAPI from '../pageobjects/IframeAPI';
import InviteDialog from '../pageobjects/InviteDialog';
@@ -534,6 +535,13 @@ export class Participant {
return new ChatPanel(this);
}
/**
* Returns the file sharing panel for this participant.
*/
getFileSharingPanel(): FileSharingPanel {
return new FileSharingPanel(this);
}
/**
* Returns the BreakoutRooms for this participant.
*

View File

@@ -22,6 +22,7 @@ const defaultExpectations = {
enabled: true
},
jaas: {
fileSharingEnabled: true,
liveStreamingEnabled: true,
recordingEnabled: true,
transcriptionEnabled: true,

View File

@@ -10,6 +10,11 @@ export type ITokenOptions = {
* The duration for which the token is valid, e.g. "1h" for one hour.
*/
exp?: string;
/**
* Additional JWT features to include (merged with the defaults).
* Keys are feature names (e.g. 'file-upload'), values are boolean or string.
*/
features?: Record<string, boolean | string>;
/**
* The key ID to use for the token.
* If not provided, the kid configured with environment variables will be used (see env.example).
@@ -72,7 +77,8 @@ export function generatePayload(options: ITokenOptions): any {
'transcription': 'true',
'recording': 'true',
'sip-outbound-call': true,
'livestreaming': true
'livestreaming': true,
...options.features
},
},
'room': options.room || '*'

View File

@@ -0,0 +1,172 @@
import BasePageObject from './BasePageObject';
const FILE_SHARING_TAB_ID = 'file_sharing-tab';
const FILE_SHARING_PANEL_ID = 'file_sharing-tab-panel';
const UPLOAD_BUTTON_LABEL = 'Share file';
const DOWNLOAD_BUTTON_LABEL = 'Download';
const REMOVE_BUTTON_LABEL = 'Remove';
/**
* Page object for the file sharing panel.
*/
export default class FileSharingPanel extends BasePageObject {
/**
* Opens the chat sidebar and navigates to the file sharing tab.
*/
async open(): Promise<void> {
if (!await this.participant.driver.$('#sideToolbarContainer').isExisting()) {
await this.participant.getToolbar().clickChatButton();
}
await this.participant.driver.$(`#${FILE_SHARING_TAB_ID}`).click();
}
/**
* Returns whether the file sharing panel is the currently active tab.
*/
async isActive(): Promise<boolean> {
return this.participant.execute(() => {
// @ts-ignore
const state = APP?.store?.getState?.();
return state?.['features/chat']?.focusedTab === 'file_sharing-tab';
});
}
/**
* Returns whether the upload button is enabled (not disabled).
*/
async isUploadButtonEnabled(): Promise<boolean> {
const btn = this.participant.driver.$(`[aria-label="${UPLOAD_BUTTON_LABEL}"]`);
return !await btn.getAttribute('disabled');
}
/**
* Uploads a file via the hidden file input.
*
* @param {string} localFilePath - Local path to the file to upload.
*/
async uploadFile(localFilePath: string): Promise<void> {
const remotePath = await this.participant.driver.uploadFile(localFilePath);
const input = this.participant.driver.$(`#${FILE_SHARING_PANEL_ID} input[type="file"]`);
await input.addValue(remotePath);
}
/**
* Waits until a file with the given name appears in the file list.
*
* @param {string} fileName - The file name to wait for.
* @param {number} timeout - Timeout in milliseconds.
*/
async waitForFile(fileName: string, timeout = 15_000): Promise<void> {
await this.participant.driver.$(`#${FILE_SHARING_PANEL_ID} [title="${fileName}"]`)
.waitForExist({ timeout, timeoutMsg: `File "${fileName}" did not appear within ${timeout}ms` });
}
/**
* Waits until a file with the given name disappears from the file list.
*
* @param {string} fileName - The file name to wait for.
* @param {number} timeout - Timeout in milliseconds.
*/
async waitForFileGone(fileName: string, timeout = 15_000): Promise<void> {
await this.participant.driver.$(`#${FILE_SHARING_PANEL_ID} [title="${fileName}"]`)
.waitForExist({ timeout, reverse: true, timeoutMsg: `File "${fileName}" still present after ${timeout}ms` });
}
/**
* Returns whether a file with the given name exists in the file list.
*
* @param {string} fileName - The file name to check.
*/
async hasFile(fileName: string): Promise<boolean> {
return this.participant.driver.$(`#${FILE_SHARING_PANEL_ID} [title="${fileName}"]`).isExisting();
}
/**
* Clicks the download button for the given file. Hovers over the file item first to make action buttons visible.
*
* @param {string} fileName - The file name to download.
*/
async downloadFile(fileName: string): Promise<void> {
await this.hoverOverFileItem(fileName);
await this.participant.driver
.$(`button[aria-label="${DOWNLOAD_BUTTON_LABEL} ${fileName}"]`)
.click();
}
/**
* Clicks the remove button for the given file. Hovers over the file item first to make action buttons visible.
*
* @param {string} fileName - The file name to remove.
*/
async removeFile(fileName: string): Promise<void> {
await this.hoverOverFileItem(fileName);
await this.participant.driver
.$(`button[aria-label="${REMOVE_BUTTON_LABEL} ${fileName}"]`)
.click();
}
/**
* Returns whether the remove button exists in the DOM for the given file.
* The remove button is only rendered when the participant has the 'file-upload' JWT feature.
*
* @param {string} fileName - The file name.
*/
canRemoveFile(fileName: string) {
return this.participant.driver
.$(`button[aria-label="${REMOVE_BUTTON_LABEL} ${fileName}"]`)
.isExisting();
}
/**
* Returns whether the download button exists in the DOM for the given file.
*
* @param {string} fileName - The file name.
*/
canDownloadFile(fileName: string) {
return this.participant.driver
.$(`button[aria-label="${DOWNLOAD_BUTTON_LABEL} ${fileName}"]`)
.isExisting();
}
/**
* Simulates dragging a file into the conference area and dispatches drag events.
* A dragenter event sets the React isDragging state, then after a short delay a
* dragover event triggers the file sharing tab to open.
*/
async simulateDragIntoConference(): Promise<void> {
await this.participant.driver.executeAsync((done: () => void) => {
const el = document.querySelector('[data-testid="conference-drag-zone"]') ?? document.body;
el.dispatchEvent(new DragEvent('dragenter', {
bubbles: true,
cancelable: true
}));
setTimeout(() => {
el.dispatchEvent(new DragEvent('dragover', {
bubbles: true,
cancelable: true
}));
done();
}, 200);
});
}
/**
* Returns whether the chat sidebar is currently open.
*/
isChatOpen() {
return this.participant.driver.$('#sideToolbarContainer').isExisting();
}
/**
* Hovers over the file item element to make its action buttons visible.
*
* @param {string} fileName - The file name whose item should be hovered.
*/
private async hoverOverFileItem(fileName: string): Promise<void> {
await this.participant.driver.$(`#${FILE_SHARING_PANEL_ID} [title="${fileName}"]`).moveTo();
}
}

View File

@@ -0,0 +1 @@
This is a test file for file sharing upload tests.

View File

@@ -0,0 +1,169 @@
import { expect } from '@wdio/globals';
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { expectations } from '../../helpers/expectations';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
usesBrowsers: [ 'p1', 'p2' ]
});
const TEST_FILE_PATH = 'tests/resources/test-upload.txt';
const TEST_FILE_NAME = 'test-upload.txt';
describe('File sharing', () => {
let p1: Participant, p2: Participant;
let fileSharingEnabled: boolean;
it('setup', async () => {
const room = ctx.roomName;
p1 = await joinJaasMuc({
name: 'p1',
token: t({ room, features: { 'file-upload': 'true' } })
});
fileSharingEnabled = await p1.execute(
() => Boolean((window as any).config?.fileSharing?.enabled && (window as any).config?.fileSharing?.apiUrl)
);
expect(fileSharingEnabled).toBe(expectations.jaas.fileSharingEnabled);
});
it('upload button enabled with file-upload feature', async () => {
if (!fileSharingEnabled) {
return;
}
const panel = p1.getFileSharingPanel();
await panel.open();
expect(await panel.isUploadButtonEnabled()).toBe(true);
});
it('upload button disabled without file-upload feature', async () => {
if (!fileSharingEnabled) {
return;
}
p2 = await joinJaasMuc({ name: 'p2', token: t({ room: ctx.roomName }) });
const panel = p2.getFileSharingPanel();
await panel.open();
expect(await panel.isUploadButtonEnabled()).toBe(false);
});
it('user with file-upload can delete files uploaded by another participant', async () => {
if (!fileSharingEnabled) {
return;
}
// p1 (with file-upload) uploads a file
const p1Panel = p1.getFileSharingPanel();
await p1Panel.open();
await p1Panel.uploadFile(TEST_FILE_PATH);
await p1Panel.waitForFile(TEST_FILE_NAME);
// p2 (joined earlier without file-upload) rejoins with file-upload to test deletion of p1's file
p2 = await joinJaasMuc({
name: 'p2',
token: t({ room: ctx.roomName, features: { 'file-upload': 'true' } })
});
const p2Panel = p2.getFileSharingPanel();
await p2Panel.open();
await p2Panel.waitForFile(TEST_FILE_NAME);
// p2 should be able to see and use the remove button for p1's file
expect(await p2Panel.canRemoveFile(TEST_FILE_NAME)).toBe(true);
await p2Panel.removeFile(TEST_FILE_NAME);
await p1Panel.waitForFileGone(TEST_FILE_NAME);
});
it('user without file-upload can download but not delete files', async () => {
if (!fileSharingEnabled) {
return;
}
// p1 (with file-upload) uploads a fresh file
const p1Panel = p1.getFileSharingPanel();
await p1Panel.open();
await p1Panel.uploadFile(TEST_FILE_PATH);
await p1Panel.waitForFile(TEST_FILE_NAME);
// p2 rejoins without file-upload
p2 = await joinJaasMuc({ name: 'p2', token: t({ room: ctx.roomName }) });
const p2Panel = p2.getFileSharingPanel();
await p2Panel.open();
await p2Panel.waitForFile(TEST_FILE_NAME);
// Download button should be present, remove button should not
expect(await p2Panel.canDownloadFile(TEST_FILE_NAME)).toBe(true);
expect(await p2Panel.canRemoveFile(TEST_FILE_NAME)).toBe(false);
});
it('dragging into conference opens file sharing tab', async () => {
if (!fileSharingEnabled) {
return;
}
// p1 already has file-upload feature
const panel = p1.getFileSharingPanel();
// Ensure chat is closed before starting
if (await panel.isChatOpen()) {
await p1.getToolbar().clickCloseChatButton();
}
await panel.simulateDragIntoConference();
await p1.driver.waitUntil(
() => panel.isChatOpen(),
{ timeout: 3000, timeoutMsg: 'Chat did not open after drag' }
);
expect(await panel.isActive()).toBe(true);
});
it('dragging on pre-join screen does not open file sharing', async () => {
if (!fileSharingEnabled) {
return;
}
// Join with iFrame API, file-upload feature, and pre-join screen enabled — but do NOT click join
p1 = await joinJaasMuc(
{
name: 'p1',
iFrameApi: true,
token: t({ room: ctx.roomName, features: { 'file-upload': 'true' } })
},
{
configOverwrite: { prejoinConfig: { enabled: true } },
skipPrejoinButtonClick: true,
skipWaitToJoin: true
}
);
// Wait for pre-join screen to appear
await p1.getPreJoinScreen().waitForLoading();
// Simulate drag while on pre-join screen
const panel = p1.getFileSharingPanel();
await panel.simulateDragIntoConference();
// Chat/file sharing should NOT have opened
expect(await panel.isChatOpen()).toBe(false);
});
});

View File

@@ -3,6 +3,8 @@ import { multiremotebrowser } from '@wdio/globals';
import { Buffer } from 'buffer';
import fs from 'fs';
import { glob } from 'glob';
import junitReportBuilder from 'junit-report-builder';
import { randomUUID } from 'node:crypto';
import path from 'node:path';
import process from 'node:process';
import pretty from 'pretty';
@@ -146,7 +148,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
// Default timeout in milliseconds for request
// if browser driver or grid doesn't send response
connectionRetryTimeout: 15_000,
connectionRetryTimeout: 30_000,
// Default request retries count
connectionRetryCount: 3,
@@ -468,6 +470,60 @@ export const config: WebdriverIO.MultiremoteConfig = {
});
},
/**
* Gets executed after a worker process has exited.
* If the worker crashed (e.g. session DELETE timeout), the JUnit reporter never flushes,
* leaving a zero-byte XML file. This hook detects that and writes a failure entry so the
* report generator has something to show.
*/
onWorkerEnd(cid, exitCode, workerSpecs) {
if (exitCode === 0) {
return;
}
const xmlPath = path.join(TEST_RESULTS_DIR, `results-${cid}.xml`);
try {
if (fs.statSync(xmlPath).size > 0) {
return;
}
} catch {
// file doesn't exist yet — fall through and create it
}
const specName = workerSpecs?.[0] ? path.basename(workerSpecs[0], '.spec.ts') : 'unknown';
const dirMatch = workerSpecs?.[0]?.match(/\/tests\/specs\/([^/]+)\//);
const dir = dirMatch ? dirMatch[1] : 'unknown';
const message = `Worker exited with code ${exitCode} before results were written. Test result is unknown - tests may have passed.`;
const b = junitReportBuilder.newBuilder();
b.testSuite().name(specName).testCase()
.name('Test runner crashed')
.className(specName)
.error(message);
b.writeTo(xmlPath);
const allureResult = {
uuid: randomUUID(),
name: 'Test runner crashed',
status: 'broken',
statusDetails: { message },
stage: 'finished',
steps: [],
attachments: [],
parameters: [],
labels: [
{ name: 'parentSuite', value: dir },
{ name: 'suite', value: specName }
],
links: []
};
const allurePath = path.join(TEST_RESULTS_DIR, 'allure-results', `${allureResult.uuid}-result.json`);
fs.writeFileSync(allurePath, JSON.stringify(allureResult));
console.log(`[onWorkerEnd] Wrote error XML and allure result for crashed worker ${cid} (spec: ${specName})`);
},
/**
* Gets executed after all workers have shut down and the process is about to exit.
* An error thrown in the `onComplete` hook will result in the test run failing.
@@ -503,7 +559,6 @@ export const config: WebdriverIO.MultiremoteConfig = {
}
});
const reportError = new Error('Could not generate Allure report');
const generation = allure([
'generate', `${TEST_RESULTS_DIR}/allure-results`,
'--clean', '--single-file',
@@ -512,15 +567,15 @@ export const config: WebdriverIO.MultiremoteConfig = {
return new Promise<void>((resolve, reject) => {
const generationTimeout = setTimeout(
() => reject(reportError),
5000);
() => reject(new Error('Could not generate Allure report: timed out after 60s')),
60_000);
// @ts-ignore
generation.on('exit', eCode => {
clearTimeout(generationTimeout);
if (eCode !== 0) {
return reject(reportError);
return reject(new Error(`Could not generate Allure report: allure exited with code ${eCode}`));
}
console.log('Allure report successfully generated');

View File

@@ -7,5 +7,5 @@ import { merge } from 'lodash-es';
import { config as defaultConfig } from './wdio.conf.ts';
export const config = merge(defaultConfig, {
baseUrl: 'https://127.0.0.1:8080/torture'
baseUrl: 'https://localhost:8080/torture'
}, { clone: false });