mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-02-11 16:30:18 +00:00
Compare commits
20 Commits
8995
...
debug-test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
417c38ab9e | ||
|
|
33a4245a1f | ||
|
|
2eb07cb79f | ||
|
|
63e4c41d92 | ||
|
|
2c6ccd7d6b | ||
|
|
4ce27eeb1a | ||
|
|
11453dcc79 | ||
|
|
3375ee49bd | ||
|
|
e45df58cfb | ||
|
|
7d628960d7 | ||
|
|
cf13b8f0ba | ||
|
|
e106109090 | ||
|
|
fc170891cb | ||
|
|
775cc52f66 | ||
|
|
3baede6ff1 | ||
|
|
9462a9ce36 | ||
|
|
4713062200 | ||
|
|
65eb2a2899 | ||
|
|
f0452d05b9 | ||
|
|
1102f4205a |
@@ -927,6 +927,9 @@ var config = {
|
||||
// [ 'microphone', 'camera' ]
|
||||
// ],
|
||||
|
||||
// Enable reduced UI on web.
|
||||
// reducedUIEnabled: true,
|
||||
|
||||
// Overrides the buttons displayed in the main toolbar for reduced UI.
|
||||
// When there isn't an override for a certain configuration the default jitsi-meet configuration will be used.
|
||||
// The order of the buttons in the array is preserved.
|
||||
|
||||
@@ -1407,7 +1407,7 @@ PODS:
|
||||
- Yoga
|
||||
- react-native-performance (5.1.2):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (5.5.2):
|
||||
- react-native-safe-area-context (5.6.1):
|
||||
- React-Core
|
||||
- react-native-slider (4.5.6):
|
||||
- DoubleConversion
|
||||
@@ -2271,7 +2271,7 @@ SPEC CHECKSUMS:
|
||||
react-native-orientation-locker: dbd3f6ddbe9e62389cb0807dc2af63f6c36dec36
|
||||
react-native-pager-view: 11662c698c8f11d39e05891316d2a144fa00adc4
|
||||
react-native-performance: 125a96c145e29918b55b45ce25cbba54f1e24dcd
|
||||
react-native-safe-area-context: 0f7bf11598f9a61b7ceac8dc3f59ef98697e99e1
|
||||
react-native-safe-area-context: 2243039f43d10cb1ea30ec5ac57fc6d1448413f4
|
||||
react-native-slider: 1205801a8d29b28cacc14eef08cb120015cdafcb
|
||||
react-native-video: eb861d67a71dfef1bbf6086a811af5f338b13781
|
||||
react-native-webrtc: e8f0ce746353adc2744a2b933645e1aeb41eaa74
|
||||
|
||||
@@ -790,7 +790,7 @@ function initCommands() {
|
||||
}
|
||||
|
||||
if (transcription) {
|
||||
APP.store.dispatch(setRequestingSubtitles(true, false, null));
|
||||
APP.store.dispatch(setRequestingSubtitles(true, false, null, true));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -812,7 +812,7 @@ function initCommands() {
|
||||
}
|
||||
|
||||
if (transcription) {
|
||||
APP.store.dispatch(setRequestingSubtitles(false, false, null));
|
||||
APP.store.dispatch(setRequestingSubtitles(false, false, null, true));
|
||||
}
|
||||
|
||||
if (mode === 'local') {
|
||||
|
||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -66,7 +66,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/v2118.0.0+67fd2c84/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
@@ -91,7 +91,7 @@
|
||||
"react-native-pager-view": "6.8.1",
|
||||
"react-native-paper": "5.10.3",
|
||||
"react-native-performance": "5.1.2",
|
||||
"react-native-safe-area-context": "5.5.2",
|
||||
"react-native-safe-area-context": "5.6.1",
|
||||
"react-native-screens": "4.11.1",
|
||||
"react-native-sound": "https://github.com/jitsi/react-native-sound.git#ea13c97b5c2a4ff5e0d9bacbd9ff5e4457fe2c3c",
|
||||
"react-native-splash-view": "0.0.18",
|
||||
@@ -18360,8 +18360,8 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2118.0.0+67fd2c84/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-V0fp3/ZJRjHZgRlpzfCcdIF4nZ+WWWkAxKuWVTMHXfcZj07aQUHTiwIukgRludtaj9RcyNcqCVC0PrGBeeUzTw==",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-wNfil8xxSjcrT3oNA5Lil0qETqR6W2XxpXNewYYjFiM2kSuWgH8fcVxgcQYzvGH+sOqEjdUEk1V81oNo/rB6tQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jitsi/js-utils": "^2.6.7",
|
||||
@@ -22002,9 +22002,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-safe-area-context": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.5.2.tgz",
|
||||
"integrity": "sha512-t4YVbHa9uAGf+pHMabGrb0uHrD5ogAusSu842oikJ3YKXcYp6iB4PTGl0EZNkUIR3pCnw/CXKn42OCfhsS0JIw==",
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz",
|
||||
"integrity": "sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
@@ -39717,8 +39717,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2118.0.0+67fd2c84/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-V0fp3/ZJRjHZgRlpzfCcdIF4nZ+WWWkAxKuWVTMHXfcZj07aQUHTiwIukgRludtaj9RcyNcqCVC0PrGBeeUzTw==",
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-wNfil8xxSjcrT3oNA5Lil0qETqR6W2XxpXNewYYjFiM2kSuWgH8fcVxgcQYzvGH+sOqEjdUEk1V81oNo/rB6tQ==",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "^2.6.7",
|
||||
"@jitsi/logger": "2.1.1",
|
||||
@@ -42378,9 +42378,9 @@
|
||||
"integrity": "sha512-l5JOJphNzox9a9icL3T6O/gEqZuqWqcbejW04WPa10m0UanBdIYrNkPFl48B3ivWw3MabpjB6GiDYv7old9/fw=="
|
||||
},
|
||||
"react-native-safe-area-context": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.5.2.tgz",
|
||||
"integrity": "sha512-t4YVbHa9uAGf+pHMabGrb0uHrD5ogAusSu842oikJ3YKXcYp6iB4PTGl0EZNkUIR3pCnw/CXKn42OCfhsS0JIw=="
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz",
|
||||
"integrity": "sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA=="
|
||||
},
|
||||
"react-native-screens": {
|
||||
"version": "4.11.1",
|
||||
|
||||
@@ -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/v2118.0.0+67fd2c84/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
@@ -97,7 +97,7 @@
|
||||
"react-native-pager-view": "6.8.1",
|
||||
"react-native-paper": "5.10.3",
|
||||
"react-native-performance": "5.1.2",
|
||||
"react-native-safe-area-context": "5.5.2",
|
||||
"react-native-safe-area-context": "5.6.1",
|
||||
"react-native-screens": "4.11.1",
|
||||
"react-native-sound": "https://github.com/jitsi/react-native-sound.git#ea13c97b5c2a4ff5e0d9bacbd9ff5e4457fe2c3c",
|
||||
"react-native-splash-view": "0.0.18",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { setRoom } from '../base/conference/actions';
|
||||
import { setRoom } from '../base/conference/actions.native';
|
||||
import { getConferenceState } from '../base/conference/functions';
|
||||
import {
|
||||
configWillLoad,
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
} from '../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../mobile/navigation/routes';
|
||||
import { clearNotifications } from '../notifications/actions';
|
||||
import { isUnsafeRoomWarningEnabled } from '../prejoin/functions';
|
||||
import { isUnsafeRoomWarningEnabled } from '../prejoin/functions.native';
|
||||
|
||||
import { maybeRedirectToTokenAuthUrl } from './actions.any';
|
||||
import { addTrackStateToURL, getDefaultURL } from './functions.native';
|
||||
|
||||
@@ -81,7 +81,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
break;
|
||||
}
|
||||
case LOCAL_PARTICIPANT_MODERATION_NOTIFICATION: {
|
||||
let descriptionKey;
|
||||
let titleKey;
|
||||
let uid = '';
|
||||
const localParticipant = getLocalParticipant(getState);
|
||||
@@ -111,8 +110,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
!raisedHand && dispatch(raiseHand(true));
|
||||
dispatch(hideNotification(uid));
|
||||
}) ],
|
||||
descriptionKey,
|
||||
sticky: true,
|
||||
titleKey,
|
||||
uid
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
@@ -271,7 +268,6 @@ StateListenerRegistry.register(
|
||||
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.hostAskedUnmute',
|
||||
sticky: true,
|
||||
customActionNameKey,
|
||||
customActionHandler,
|
||||
uid: ASKED_TO_UNMUTE_NOTIFICATION_ID
|
||||
|
||||
@@ -93,10 +93,17 @@ export function commonUserJoinedHandling(
|
||||
if (!user.isHidden()) {
|
||||
const isReplacing = user?.isReplacing();
|
||||
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: user.getIdentity()?.user?.avatar,
|
||||
avatarURL: userIdentity?.avatar,
|
||||
botType: user.getBotType(),
|
||||
conference,
|
||||
id,
|
||||
@@ -105,7 +112,8 @@ export function commonUserJoinedHandling(
|
||||
role: user.getRole(),
|
||||
isPromoted,
|
||||
isReplacing,
|
||||
sources: user.getSources()
|
||||
sources: user.getSources(),
|
||||
userContext
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,6 +559,7 @@ export interface IConfig {
|
||||
skipConsentInMeeting?: boolean;
|
||||
suggestRecording?: boolean;
|
||||
};
|
||||
reducedUIEnabled?: boolean;
|
||||
reducedUImainToolbarButtons?: Array<string>;
|
||||
remoteVideoMenu?: {
|
||||
disableDemote?: boolean;
|
||||
|
||||
@@ -215,6 +215,7 @@ export default [
|
||||
'recordings.showPrejoinWarning',
|
||||
'recordings.showRecordingLink',
|
||||
'recordings.suggestRecording',
|
||||
'reducedUIEnabled',
|
||||
'reducedUImainToolbarButtons',
|
||||
'replaceParticipant',
|
||||
'resolution',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getCustomerDetails } from '../../jaas/actions.any';
|
||||
import { getJaasJWT, isVpaasMeeting } from '../../jaas/functions';
|
||||
import { navigateRoot } from '../../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../../mobile/navigation/routes';
|
||||
import { conferenceWillLeave } from '../conference/actions.native';
|
||||
import { setJWT } from '../jwt/actions';
|
||||
import { JitsiConnectionErrors } from '../lib-jitsi-meet';
|
||||
|
||||
@@ -58,5 +59,8 @@ export function connect(id?: string, password?: string) {
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function hangup(_requestFeedback = false) {
|
||||
return (dispatch: IStore['dispatch']) => dispatch(appNavigate(undefined));
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
dispatch(conferenceWillLeave());
|
||||
dispatch(appNavigate(undefined));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ const JitsiScreen = ({
|
||||
footerComponent,
|
||||
hasBottomTextInput = false,
|
||||
hasExtraHeaderHeight = false,
|
||||
safeAreaInsets = [ 'left', 'right' ],
|
||||
safeAreaInsets = [ 'bottom', 'left', 'right' ],
|
||||
style
|
||||
}: IProps) => {
|
||||
const renderContent = () => (
|
||||
@@ -78,8 +78,8 @@ const JitsiScreen = ({
|
||||
edges = { safeAreaInsets }
|
||||
style = { styles.safeArea }>
|
||||
{ children }
|
||||
{ footerComponent?.() }
|
||||
</SafeAreaView>
|
||||
{ footerComponent?.() }
|
||||
</JitsiKeyboardAvoidingView>
|
||||
);
|
||||
|
||||
|
||||
@@ -606,13 +606,21 @@ function _e2eeUpdated({ getState, dispatch }: IStore, conference: IJitsiConferen
|
||||
function _localParticipantJoined({ getState, dispatch }: IStore, next: Function, action: AnyAction) {
|
||||
const result = next(action);
|
||||
|
||||
const settings = getState()['features/base/settings'];
|
||||
const state = getState();
|
||||
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: ''
|
||||
id: '',
|
||||
userContext
|
||||
}));
|
||||
|
||||
return result;
|
||||
|
||||
@@ -627,7 +627,8 @@ function _participantJoined({ participant }: { participant: IParticipant; }) {
|
||||
pinned,
|
||||
presence,
|
||||
role,
|
||||
sources
|
||||
sources,
|
||||
userContext
|
||||
} = participant;
|
||||
let { conference, id } = participant;
|
||||
|
||||
@@ -659,7 +660,8 @@ function _participantJoined({ participant }: { participant: IParticipant; }) {
|
||||
pinned: pinned || false,
|
||||
presence,
|
||||
role: role || PARTICIPANT_ROLE.NONE,
|
||||
sources
|
||||
sources,
|
||||
userContext
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,12 @@ export interface IParticipant {
|
||||
role?: string;
|
||||
sources?: Map<string, Map<string, ISourceInfo>>;
|
||||
supportsRemoteControl?: boolean;
|
||||
userContext?: IUserContext;
|
||||
}
|
||||
|
||||
export interface IUserContext {
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface ILocalParticipant extends IParticipant {
|
||||
|
||||
@@ -43,6 +43,7 @@ export function clientResized(clientWidth: number, clientHeight: number) {
|
||||
|
||||
if (navigator.product !== 'ReactNative') {
|
||||
const state = getState();
|
||||
const { reducedUIEnabled = true } = state['features/base/config'];
|
||||
const { isOpen: isChatOpen, width } = state['features/chat'];
|
||||
|
||||
if (isChatOpen) {
|
||||
@@ -51,7 +52,7 @@ export function clientResized(clientWidth: number, clientHeight: number) {
|
||||
|
||||
availableWidth -= getParticipantsPaneWidth(state);
|
||||
|
||||
dispatch(setReducedUI(availableWidth, clientHeight));
|
||||
reducedUIEnabled && dispatch(setReducedUI(availableWidth, clientHeight));
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
@@ -112,7 +113,7 @@ export function setReducedUI(width: number, height: number) {
|
||||
const threshold = navigator.product === 'ReactNative'
|
||||
? REDUCED_UI_THRESHOLD
|
||||
: WEB_REDUCED_UI_THRESHOLD;
|
||||
const reducedUI = Math.min(width, height) < threshold;
|
||||
const reducedUI = Math.max(width, height) < threshold;
|
||||
|
||||
if (reducedUI !== getState()['features/base/responsive-ui'].reducedUI) {
|
||||
return dispatch({
|
||||
|
||||
@@ -8,7 +8,7 @@ export default {
|
||||
|
||||
button: {
|
||||
marginBottom: BaseTheme.spacing[4],
|
||||
marginHorizontal: BaseTheme.spacing[2]
|
||||
marginHorizontal: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
collapsibleList: {
|
||||
|
||||
@@ -44,6 +44,16 @@ export const getMainRoom = (stateful: IStateful) => {
|
||||
* @returns {IRoomsInfo} The rooms info.
|
||||
*/
|
||||
export const getRoomsInfo = (stateful: IStateful) => {
|
||||
const state = toState(stateful);
|
||||
const localParticipant = getLocalParticipant(stateful);
|
||||
const jwtUser = state['features/base/jwt']?.user;
|
||||
const localUserContext = jwtUser ? {
|
||||
id: jwtUser.id,
|
||||
name: jwtUser.name
|
||||
} : {
|
||||
id: localParticipant?.jwtId,
|
||||
name: localParticipant?.name
|
||||
};
|
||||
const breakoutRooms = getBreakoutRooms(stateful);
|
||||
const conference = getCurrentConference(stateful);
|
||||
|
||||
@@ -57,7 +67,6 @@ export const getRoomsInfo = (stateful: IStateful) => {
|
||||
const conferenceParticipants = conference?.getParticipants()
|
||||
.filter((participant: IJitsiParticipant) => !participant.isHidden());
|
||||
|
||||
const localParticipant = getLocalParticipant(stateful);
|
||||
let localParticipantInfo;
|
||||
|
||||
if (localParticipant) {
|
||||
@@ -65,7 +74,8 @@ export const getRoomsInfo = (stateful: IStateful) => {
|
||||
role: localParticipant.role,
|
||||
displayName: localParticipant.name,
|
||||
avatarUrl: localParticipant.loadableAvatarUrl,
|
||||
id: localParticipant.id
|
||||
id: localParticipant.id,
|
||||
userContext: localUserContext
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,7 +96,8 @@ export const getRoomsInfo = (stateful: IStateful) => {
|
||||
role: participantItem.getRole(),
|
||||
displayName: participantItem.getDisplayName(),
|
||||
avatarUrl: storeParticipant?.loadableAvatarUrl,
|
||||
id: participantItem.getId()
|
||||
id: participantItem.getId(),
|
||||
userContext: storeParticipant?.userContext
|
||||
} as IRoomInfoParticipant;
|
||||
}) ]
|
||||
: [ localParticipantInfo ]
|
||||
@@ -110,13 +121,18 @@ export const getRoomsInfo = (stateful: IStateful) => {
|
||||
const storeParticipant = getParticipantById(stateful,
|
||||
ids.length > 1 ? ids[1] : participantItem.jid);
|
||||
|
||||
// Check if this is the local participant
|
||||
const isLocal = storeParticipant?.id === localParticipant?.id;
|
||||
const userContext = isLocal ? localUserContext : (storeParticipant?.userContext || participantItem.userContext);
|
||||
|
||||
return {
|
||||
jid: participantItem?.jid,
|
||||
role: participantItem?.role,
|
||||
displayName: participantItem?.displayName,
|
||||
avatarUrl: storeParticipant?.loadableAvatarUrl,
|
||||
id: storeParticipant ? storeParticipant.id
|
||||
: participantLongId
|
||||
: participantLongId,
|
||||
userContext
|
||||
} as IRoomInfoParticipant;
|
||||
}) : []
|
||||
} as IRoomInfo;
|
||||
|
||||
@@ -44,19 +44,59 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
|
||||
switch (type) {
|
||||
case UPDATE_BREAKOUT_ROOMS: {
|
||||
// edit name if it was overwritten
|
||||
// Enrich participants with userContext from Redux store
|
||||
if (!action.updatedNames) {
|
||||
const { overwrittenNameList } = getState()['features/base/participants'];
|
||||
const state = getState();
|
||||
const { overwrittenNameList, local: localParticipant } = state['features/base/participants'];
|
||||
const jwtUser = state['features/base/jwt']?.user;
|
||||
const localUserContext = jwtUser ? {
|
||||
id: jwtUser.id,
|
||||
name: jwtUser.name
|
||||
} : {
|
||||
id: localParticipant?.jwtId,
|
||||
name: localParticipant?.name
|
||||
};
|
||||
|
||||
if (Object.keys(overwrittenNameList).length > 0) {
|
||||
const newRooms: IRooms = {};
|
||||
// Get existing userContext cache
|
||||
const existingCache = state['features/breakout-rooms'].userContextCache || {};
|
||||
const newCache = { ...existingCache };
|
||||
|
||||
Object.entries(action.rooms as IRooms).forEach(([ key, r ]) => {
|
||||
let participants = r?.participants || {};
|
||||
let jid;
|
||||
const newRooms: IRooms = {};
|
||||
|
||||
Object.entries(action.rooms as IRooms).forEach(([ key, r ]) => {
|
||||
let participants = r?.participants || {};
|
||||
|
||||
// Add userContext to each participant
|
||||
const enhancedParticipants: typeof participants = {};
|
||||
|
||||
for (const [ participantJid, participantData ] of Object.entries(participants)) {
|
||||
const ids = participantJid.split('/');
|
||||
const participantId = ids.length > 1 ? ids[1] : participantData.jid;
|
||||
const storeParticipant = getParticipantById(state, participantId);
|
||||
const isLocal = storeParticipant?.id === localParticipant?.id;
|
||||
|
||||
// Try to get userContext from: local, store, cache, or incoming data
|
||||
const userContext = isLocal
|
||||
? localUserContext
|
||||
: (storeParticipant?.userContext || newCache[participantId] || participantData.userContext);
|
||||
|
||||
// Update cache if we have userContext
|
||||
if (userContext && participantId) {
|
||||
newCache[participantId] = userContext;
|
||||
}
|
||||
|
||||
enhancedParticipants[participantJid] = {
|
||||
...participantData,
|
||||
userContext
|
||||
};
|
||||
}
|
||||
|
||||
participants = enhancedParticipants;
|
||||
|
||||
// Apply overwritten display names
|
||||
if (Object.keys(overwrittenNameList).length > 0) {
|
||||
for (const id of Object.keys(overwrittenNameList)) {
|
||||
jid = Object.keys(participants).find(p => p.slice(p.indexOf('/') + 1) === id);
|
||||
const jid = Object.keys(participants).find(p => p.slice(p.indexOf('/') + 1) === id);
|
||||
|
||||
if (jid) {
|
||||
participants = {
|
||||
@@ -68,15 +108,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newRooms[key] = {
|
||||
...r,
|
||||
participants
|
||||
};
|
||||
});
|
||||
newRooms[key] = {
|
||||
...r,
|
||||
participants
|
||||
};
|
||||
});
|
||||
|
||||
action.rooms = newRooms;
|
||||
}
|
||||
action.rooms = newRooms;
|
||||
action.userContextCache = newCache;
|
||||
}
|
||||
|
||||
// edit the chat history to match names for participants in breakout rooms
|
||||
|
||||
@@ -10,12 +10,19 @@ import { IRooms } from './types';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
rooms: {},
|
||||
roomCounter: 0
|
||||
roomCounter: 0,
|
||||
userContextCache: {}
|
||||
};
|
||||
|
||||
export interface IBreakoutRoomsState {
|
||||
roomCounter: number;
|
||||
rooms: IRooms;
|
||||
userContextCache: {
|
||||
[participantId: string]: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,12 +36,13 @@ ReducerRegistry.register<IBreakoutRoomsState>(FEATURE_KEY, (state = DEFAULT_STAT
|
||||
roomCounter: action.roomCounter
|
||||
};
|
||||
case UPDATE_BREAKOUT_ROOMS: {
|
||||
const { roomCounter, rooms } = action;
|
||||
const { roomCounter, rooms, userContextCache } = action;
|
||||
|
||||
return {
|
||||
...state,
|
||||
roomCounter,
|
||||
rooms
|
||||
rooms,
|
||||
userContextCache: userContextCache || state.userContextCache
|
||||
};
|
||||
}
|
||||
case _RESET_BREAKOUT_ROOMS: {
|
||||
|
||||
@@ -8,6 +8,10 @@ export interface IRoom {
|
||||
displayName: string;
|
||||
jid: string;
|
||||
role: string;
|
||||
userContext?: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -33,4 +37,8 @@ export interface IRoomInfoParticipant {
|
||||
id: string;
|
||||
jid: string;
|
||||
role: string;
|
||||
userContext?: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,39 +1,29 @@
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
import { Route, useIsFocused } from '@react-navigation/native';
|
||||
import React, { Component, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import { StyleType } from '../../../base/styles/functions.native';
|
||||
import { TabBarLabelCounter } from '../../../mobile/navigation/components/TabBarLabelCounter';
|
||||
import { getUnreadPollCount } from '../../../polls/functions';
|
||||
import { pollsStyles } from '../../../polls/components/native/styles';
|
||||
import { closeChat, sendMessage } from '../../actions.native';
|
||||
import { getUnreadFilesCount } from '../../functions';
|
||||
import { ChatTabs } from '../../constants';
|
||||
import { IChatProps as AbstractProps } from '../../types';
|
||||
|
||||
import ChatInputBar from './ChatInputBar';
|
||||
import MessageContainer from './MessageContainer';
|
||||
import MessageRecipient from './MessageRecipient';
|
||||
import styles from './styles';
|
||||
|
||||
interface IProps extends AbstractProps {
|
||||
|
||||
/**
|
||||
* The number of unread file messages.
|
||||
*/
|
||||
_unreadFilesCount: number;
|
||||
|
||||
/**
|
||||
* The number of unread messages.
|
||||
*/
|
||||
_unreadMessagesCount: number;
|
||||
|
||||
/**
|
||||
* The number of unread polls.
|
||||
*/
|
||||
_unreadPollsCount: number;
|
||||
|
||||
/**
|
||||
* Default prop for navigating between screen components(React Navigation).
|
||||
*/
|
||||
@@ -62,6 +52,7 @@ class Chat extends Component<IProps> {
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onSendMessage = this._onSendMessage.bind(this);
|
||||
this._renderFooter = this._renderFooter.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,14 +67,10 @@ class Chat extends Component<IProps> {
|
||||
return (
|
||||
<JitsiScreen
|
||||
disableForcedKeyboardDismiss = { true }
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
footerComponent = { () =>
|
||||
<ChatInputBar onSend = { this._onSendMessage } />
|
||||
}
|
||||
footerComponent = { this._renderFooter }
|
||||
hasBottomTextInput = { true }
|
||||
hasExtraHeaderHeight = { true }
|
||||
style = { styles.chatContainer }>
|
||||
style = { pollsStyles.pollPaneContainer as StyleType }>
|
||||
{/* @ts-ignore */}
|
||||
<MessageContainer messages = { _messages } />
|
||||
<MessageRecipient privateMessageRecipient = { privateMessageRecipient } />
|
||||
@@ -91,6 +78,16 @@ class Chat extends Component<IProps> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the footer component.
|
||||
*
|
||||
* @private
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderFooter() {
|
||||
return <ChatInputBar onSend = { this._onSendMessage } />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a text message.
|
||||
*
|
||||
@@ -113,9 +110,7 @@ class Chat extends Component<IProps> {
|
||||
* @private
|
||||
* @returns {{
|
||||
* _messages: Array<Object>,
|
||||
* _unreadMessagesCount: number,
|
||||
* _unreadPollsCount: number,
|
||||
* _unreadFilesCount: number
|
||||
* _unreadMessagesCount: number
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
@@ -123,34 +118,34 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
|
||||
return {
|
||||
_messages: messages,
|
||||
_unreadMessagesCount: unreadMessagesCount,
|
||||
_unreadPollsCount: getUnreadPollCount(state),
|
||||
_unreadFilesCount: getUnreadFilesCount(state)
|
||||
_unreadMessagesCount: unreadMessagesCount
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)((props: IProps) => {
|
||||
const { _unreadMessagesCount, _unreadPollsCount, _unreadFilesCount, dispatch, navigation, t } = props;
|
||||
const totalUnread = _unreadMessagesCount + _unreadPollsCount + _unreadFilesCount;
|
||||
const unreadMessagesNr = totalUnread > 0;
|
||||
const { _unreadMessagesCount, dispatch, navigation, t } = props;
|
||||
|
||||
const isChatTabFocused = useSelector((state: IReduxState) => state['features/chat'].focusedTab === ChatTabs.CHAT);
|
||||
|
||||
const isFocused = useIsFocused();
|
||||
|
||||
const activeUnreadMessagesNr = !isChatTabFocused && _unreadMessagesCount > 0;
|
||||
|
||||
useEffect(() => {
|
||||
navigation?.setOptions({
|
||||
tabBarLabel: () => (
|
||||
<TabBarLabelCounter
|
||||
activeUnreadNr = { unreadMessagesNr }
|
||||
activeUnreadNr = { activeUnreadMessagesNr }
|
||||
isFocused = { isFocused }
|
||||
label = { t('chat.tabs.chat') }
|
||||
unreadCount = { totalUnread } />
|
||||
unreadCount = { _unreadMessagesCount } />
|
||||
)
|
||||
});
|
||||
|
||||
return () => {
|
||||
isFocused && dispatch(closeChat());
|
||||
};
|
||||
}, [ isFocused, _unreadMessagesCount, _unreadPollsCount, _unreadFilesCount ]);
|
||||
}, [ isFocused, _unreadMessagesCount ]);
|
||||
|
||||
return (
|
||||
<Chat { ...props } />
|
||||
|
||||
@@ -7,7 +7,6 @@ import { connect } from 'react-redux';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconSend } from '../../../base/icons/svg';
|
||||
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants';
|
||||
import IconButton from '../../../base/ui/components/native/IconButton';
|
||||
import Input from '../../../base/ui/components/native/Input';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
@@ -85,14 +84,6 @@ class ChatInputBar extends Component<IProps, IState> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
let inputBarStyles;
|
||||
|
||||
if (this.props.aspectRatio === ASPECT_RATIO_WIDE) {
|
||||
inputBarStyles = styles.inputBarWide;
|
||||
} else {
|
||||
inputBarStyles = styles.inputBarNarrow;
|
||||
}
|
||||
|
||||
if (this.props._isSendGroupChatDisabled && !this.props._privateMessageRecipientId) {
|
||||
return (
|
||||
<View
|
||||
@@ -109,7 +100,7 @@ class ChatInputBar extends Component<IProps, IState> {
|
||||
<View
|
||||
id = 'chat-input'
|
||||
style = { [
|
||||
inputBarStyles,
|
||||
styles.inputBar,
|
||||
this.state.addPadding ? styles.extraBarPadding : null
|
||||
] as ViewStyle[] }>
|
||||
<Input
|
||||
@@ -129,6 +120,7 @@ class ChatInputBar extends Component<IProps, IState> {
|
||||
id = { this.props.t('chat.sendButton') }
|
||||
onPress = { this._onSubmit }
|
||||
src = { IconSend }
|
||||
style = { styles.sendButton }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -42,6 +42,8 @@ const ClosedCaptions = ({
|
||||
const navigateToLanguageSelect = useCallback(() => {
|
||||
navigate(screen.conference.subtitles);
|
||||
}, [ navigation, screen ]);
|
||||
const isAsyncTranscriptionEnabled = useSelector((state: IReduxState) =>
|
||||
state['features/base/conference'].conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription);
|
||||
|
||||
useEffect(() => {
|
||||
navigation?.setOptions({
|
||||
@@ -91,17 +93,20 @@ const ClosedCaptions = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style = { closedCaptionsStyles.languageButtonContainer as ViewStyle }>
|
||||
<Text style = { closedCaptionsStyles.languageButtonText }>{ t('transcribing.translateTo') }:</Text>
|
||||
<TouchableHighlight onPress = { navigateToLanguageSelect }>
|
||||
<View style = { closedCaptionsStyles.languageButtonContent as ViewStyle }>
|
||||
<Text style = { closedCaptionsStyles.languageButtonText }>{ t(selectedLanguage ?? 'transcribing.subtitlesOff') }</Text>
|
||||
<Icon
|
||||
size = { 24 }
|
||||
src = { IconArrowRight } />
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
{
|
||||
// Hide the "Translate to" option when asyncTranscription is enabled
|
||||
!isAsyncTranscriptionEnabled && <View style = { closedCaptionsStyles.languageButtonContainer as ViewStyle }>
|
||||
<Text style = { closedCaptionsStyles.languageButtonText }>{ t('transcribing.translateTo') }:</Text>
|
||||
<TouchableHighlight onPress = { navigateToLanguageSelect }>
|
||||
<View style = { closedCaptionsStyles.languageButtonContent as ViewStyle }>
|
||||
<Text style = { closedCaptionsStyles.languageButtonText }>{ t(selectedLanguage ?? 'transcribing.subtitlesOff') }</Text>
|
||||
<Icon
|
||||
size = { 24 }
|
||||
src = { IconArrowRight } />
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
}
|
||||
<View style = { closedCaptionsStyles.messagesContainer as ViewStyle }>
|
||||
<SubtitlesMessagesContainer
|
||||
groups = { groupedSubtitles }
|
||||
|
||||
@@ -14,12 +14,6 @@ const recipientContainer = {
|
||||
padding: BaseTheme.spacing[2]
|
||||
};
|
||||
|
||||
const inputBar = {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
};
|
||||
|
||||
/**
|
||||
* The styles of the feature chat.
|
||||
*
|
||||
@@ -143,23 +137,23 @@ export default {
|
||||
* A special padding to avoid issues on some devices (such as Android devices with custom suggestions bar).
|
||||
*/
|
||||
extraBarPadding: {
|
||||
paddingBottom: 30
|
||||
paddingBottom: BaseTheme.spacing[8]
|
||||
},
|
||||
|
||||
inputBarNarrow: {
|
||||
...inputBar,
|
||||
height: 112,
|
||||
marginHorizontal: BaseTheme.spacing[3]
|
||||
inputBar: {
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
inputBarWide: {
|
||||
...inputBar,
|
||||
height: 88,
|
||||
marginHorizontal: BaseTheme.spacing[9]
|
||||
sendButton: {
|
||||
marginRight: BaseTheme.spacing[5],
|
||||
marginLeft: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
customInputContainer: {
|
||||
width: '75%'
|
||||
marginLeft: BaseTheme.spacing[5],
|
||||
flex: 1
|
||||
},
|
||||
|
||||
messageBubble: {
|
||||
@@ -215,11 +209,6 @@ export default {
|
||||
fontSize: 13
|
||||
},
|
||||
|
||||
chatContainer: {
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
tabContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center'
|
||||
|
||||
@@ -21,10 +21,9 @@ const titleBarSafeView = {
|
||||
export default {
|
||||
|
||||
bottomContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
bottom: BaseTheme.spacing[8]
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -101,7 +100,7 @@ export default {
|
||||
},
|
||||
|
||||
soundDeviceButton: {
|
||||
marginBottom: BaseTheme.spacing[3],
|
||||
marginVertical: BaseTheme.spacing[3],
|
||||
width: 240
|
||||
},
|
||||
|
||||
@@ -162,7 +161,6 @@ export default {
|
||||
videoStoppedLabel: {
|
||||
...BaseTheme.typography.bodyShortRegularLarge,
|
||||
color: BaseTheme.palette.text01,
|
||||
marginBottom: BaseTheme.spacing[3],
|
||||
textAlign: 'center',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
@@ -257,6 +257,9 @@ class Conference extends AbstractConference<IProps, any> {
|
||||
id = 'videospace'
|
||||
onTouchStart = { this._onVideospaceTouchStart }>
|
||||
<LargeVideo />
|
||||
<StageFilmstrip />
|
||||
<ScreenshareFilmstrip />
|
||||
<MainFilmstrip />
|
||||
</div>
|
||||
<span
|
||||
aria-level = { 1 }
|
||||
|
||||
@@ -212,7 +212,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const state = store.getState();
|
||||
const { defaultRemoteDisplayName } = state['features/base/config'];
|
||||
const { participant } = action;
|
||||
const { fakeParticipant, id, local, name } = participant;
|
||||
const { fakeParticipant, id, local, name, userContext } = participant;
|
||||
|
||||
// The version of external api outside of middleware did not emit
|
||||
// the local participant being created.
|
||||
@@ -225,7 +225,8 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
APP.API.notifyUserJoined(id, {
|
||||
displayName: name,
|
||||
formattedDisplayName: appendSuffix(
|
||||
name || defaultRemoteDisplayName)
|
||||
name || defaultRemoteDisplayName),
|
||||
userContext
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -338,6 +338,11 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_maxTopPanelHeight: number;
|
||||
|
||||
/**
|
||||
* Whethere reduced UI feature is enabled or not.
|
||||
*/
|
||||
_reducedUI: boolean;
|
||||
|
||||
/**
|
||||
* The participants in the call.
|
||||
*/
|
||||
@@ -546,6 +551,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
_filmstripDisabled,
|
||||
_localScreenShareId,
|
||||
_mainFilmstripVisible,
|
||||
_reducedUI,
|
||||
_resizableFilmstrip,
|
||||
_topPanelFilmstrip,
|
||||
_topPanelMaxHeight,
|
||||
@@ -589,6 +595,13 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
// FIX: Until we move AudioTracksContainer to a more global place,
|
||||
// we apply this css hot fix to hide Filmstrip but keep AudioTracksContainer in the DOM,
|
||||
// so we don't have audio problems when reduced UI is enabled.
|
||||
if (_reducedUI) {
|
||||
filmstripStyle.display = 'none';
|
||||
}
|
||||
|
||||
let toolbar: React.ReactNode = null;
|
||||
|
||||
if (!this.props._iAmRecorder && this.props._isFilmstripButtonEnabled
|
||||
@@ -1120,6 +1133,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
const _isVerticalFilmstrip = _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
|
||||
|| (filmstripType === FILMSTRIP_TYPE.MAIN && _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW);
|
||||
const { reducedUI } = state['features/base/responsive-ui'];
|
||||
|
||||
return {
|
||||
_className: className,
|
||||
@@ -1137,6 +1151,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
_mainFilmstripVisible: notDisabled,
|
||||
_maxFilmstripWidth: videoSpaceWidth - MIN_STAGE_VIEW_WIDTH,
|
||||
_maxTopPanelHeight: clientHeight - MIN_STAGE_VIEW_HEIGHT,
|
||||
_reducedUI: reducedUI,
|
||||
_remoteParticipantsLength: _remoteParticipants?.length ?? 0,
|
||||
_topPanelHeight: topPanelHeight.current,
|
||||
_topPanelMaxHeight: topPanelHeight.current || TOP_FILMSTRIP_HEIGHT,
|
||||
|
||||
@@ -24,8 +24,6 @@ export default {
|
||||
flexDirection: 'row',
|
||||
height: 56,
|
||||
justifyContent: 'center',
|
||||
marginBottom: BaseTheme.spacing[0],
|
||||
paddingBottom: BaseTheme.spacing[4],
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { WithTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
SafeAreaView,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle
|
||||
@@ -513,7 +512,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<IProps, IState> {
|
||||
*/
|
||||
_renderShareMeetingButton() {
|
||||
return (
|
||||
<SafeAreaView
|
||||
<View
|
||||
style = { [
|
||||
styles.bottomBar as ViewStyle,
|
||||
this.state.bottomPadding ? styles.extraBarPadding : null
|
||||
@@ -524,7 +523,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<IProps, IState> {
|
||||
src = { IconShare }
|
||||
style = { styles.shareIcon } />
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
SET_PASSWORD
|
||||
} from '../base/conference/actionTypes';
|
||||
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
|
||||
@@ -69,11 +70,13 @@ ReducerRegistry.register<ILobbyState>('features/lobby', (state = DEFAULT_STATE,
|
||||
}
|
||||
case CONFERENCE_JOINED:
|
||||
case CONFERENCE_LEFT:
|
||||
case CONFERENCE_WILL_LEAVE:
|
||||
return {
|
||||
...state,
|
||||
isDisplayNameRequiredError: false,
|
||||
knocking: false,
|
||||
passwordJoinFailed: false
|
||||
lobbyVisible: false,
|
||||
passwordJoinFailed: false,
|
||||
};
|
||||
case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED:
|
||||
return _knockingParticipantArrivedOrUpdated(action.participant, state);
|
||||
|
||||
@@ -588,7 +588,7 @@ function _registerForNativeEvents(store: IStore) {
|
||||
}
|
||||
|
||||
if (transcription) {
|
||||
store.dispatch(setRequestingSubtitles(true, false, null));
|
||||
store.dispatch(setRequestingSubtitles(true, false, null, true));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -603,7 +603,7 @@ function _registerForNativeEvents(store: IStore) {
|
||||
}
|
||||
|
||||
if (transcription) {
|
||||
store.dispatch(setRequestingSubtitles(false, false, null));
|
||||
store.dispatch(setRequestingSubtitles(false, false, null, true));
|
||||
}
|
||||
|
||||
if (![ JitsiRecordingConstants.mode.FILE, JitsiRecordingConstants.mode.STREAM ].includes(mode)) {
|
||||
|
||||
@@ -13,7 +13,6 @@ export interface INotificationProps {
|
||||
hideErrorSupportLink?: boolean;
|
||||
icon?: string;
|
||||
maxLines?: number;
|
||||
sticky?: boolean;
|
||||
title?: string;
|
||||
titleArguments?: {
|
||||
[key: string]: string | number;
|
||||
|
||||
@@ -27,7 +27,7 @@ const LobbyParticipantList = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style = { styles.meetingListContainer }>
|
||||
<View style = { styles.listDetails as ViewStyle } >
|
||||
<Text style = { styles.lobbyListDescription as TextStyle }>
|
||||
{ title }
|
||||
@@ -50,7 +50,7 @@ const LobbyParticipantList = () => {
|
||||
participant = { p } />)
|
||||
)
|
||||
}
|
||||
</>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -94,8 +94,6 @@ function ParticipantItem({
|
||||
}: IProps) {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const participantNameContainerStyles
|
||||
= isKnockingParticipant ? styles.lobbyParticipantNameContainer : styles.participantNameContainer;
|
||||
|
||||
return (
|
||||
<View style = { styles.participantContainer as StyleProp<ViewStyle> } >
|
||||
@@ -106,12 +104,8 @@ function ParticipantItem({
|
||||
displayName = { displayName }
|
||||
participantId = { participantID }
|
||||
size = { 32 } />
|
||||
<View
|
||||
style = { [
|
||||
styles.participantDetailsContainer,
|
||||
raisedHand && styles.participantDetailsContainerRaisedHand
|
||||
] as StyleProp<ViewStyle> }>
|
||||
<View style = { participantNameContainerStyles as StyleProp<ViewStyle> }>
|
||||
<View>
|
||||
<View style = { styles.participantNameContainer as StyleProp<ViewStyle> }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.participantName as StyleProp<TextStyle> }>
|
||||
@@ -128,13 +122,11 @@ function ParticipantItem({
|
||||
</View>
|
||||
{
|
||||
!isKnockingParticipant
|
||||
&& <>
|
||||
{ raisedHand && <RaisedHandIndicator /> }
|
||||
<View style = { styles.participantStatesContainer as StyleProp<ViewStyle> }>
|
||||
<View style = { styles.participantStateVideo }>{ VideoStateIcons[videoMediaState] }</View>
|
||||
<View>{ AudioStateIcons[audioMediaState] }</View>
|
||||
</View>
|
||||
</>
|
||||
&& <View style = { styles.participantStatesContainer as StyleProp<ViewStyle> }>
|
||||
<View>{ raisedHand && <RaisedHandIndicator /> }</View>
|
||||
<View style = { styles.participantStateVideo }>{ VideoStateIcons[videoMediaState] }</View>
|
||||
<View>{ AudioStateIcons[audioMediaState] }</View>
|
||||
</View>
|
||||
}
|
||||
</TouchableOpacity>
|
||||
{ !local && children }
|
||||
|
||||
@@ -6,12 +6,8 @@ import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
const participantListDescription = {
|
||||
...BaseTheme.typography.heading6,
|
||||
color: BaseTheme.palette.text01,
|
||||
fontSize: 15,
|
||||
fontWeight: 'bold',
|
||||
marginLeft: BaseTheme.spacing[2],
|
||||
marginVertical: BaseTheme.spacing[2],
|
||||
position: 'relative',
|
||||
width: '70%'
|
||||
paddingVertical: BaseTheme.spacing[2]
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -52,13 +48,6 @@ const contextMenuItem = {
|
||||
marginLeft: BaseTheme.spacing[3]
|
||||
};
|
||||
|
||||
const participantNameContainer = {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
paddingLeft: BaseTheme.spacing[3]
|
||||
};
|
||||
|
||||
/**
|
||||
* The styles of the native components of the feature {@code participants}.
|
||||
*/
|
||||
@@ -91,8 +80,7 @@ export default {
|
||||
flexDirection: 'row',
|
||||
height: BaseTheme.spacing[9],
|
||||
paddingLeft: BaseTheme.spacing[3],
|
||||
paddingRight: BaseTheme.spacing[3],
|
||||
width: '100%'
|
||||
paddingRight: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
participantContent: {
|
||||
@@ -106,24 +94,11 @@ export default {
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
participantDetailsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '73%'
|
||||
},
|
||||
|
||||
participantDetailsContainerRaisedHand: {
|
||||
width: '65%'
|
||||
},
|
||||
|
||||
participantNameContainer: {
|
||||
...participantNameContainer,
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
lobbyParticipantNameContainer: {
|
||||
...participantNameContainer,
|
||||
width: '40%'
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
paddingLeft: BaseTheme.spacing[3],
|
||||
},
|
||||
|
||||
participantName: {
|
||||
@@ -139,19 +114,19 @@ export default {
|
||||
},
|
||||
|
||||
participantStatesContainer: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
marginLeft: 'auto',
|
||||
width: '15%'
|
||||
marginLeft: 'auto'
|
||||
},
|
||||
|
||||
participantStateVideo: {
|
||||
paddingRight: BaseTheme.spacing[3]
|
||||
marginRight: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
raisedHandIndicator: {
|
||||
backgroundColor: BaseTheme.palette.warning02,
|
||||
borderRadius: BaseTheme.shape.borderRadius / 2,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
height: BaseTheme.spacing[4],
|
||||
width: BaseTheme.spacing[4],
|
||||
marginLeft: 'auto',
|
||||
@@ -166,7 +141,7 @@ export default {
|
||||
|
||||
buttonAdmit: {
|
||||
position: 'absolute',
|
||||
right: 16
|
||||
right: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
buttonReject: {
|
||||
@@ -201,20 +176,14 @@ export default {
|
||||
},
|
||||
|
||||
participantsPaneFooterContainer: {
|
||||
alignItems: 'center',
|
||||
bottom: 0,
|
||||
height: 128,
|
||||
left: 0,
|
||||
paddingHorizontal: BaseTheme.spacing[4],
|
||||
right: 0
|
||||
alignSelf: 'stretch',
|
||||
marginHorizontal: BaseTheme.spacing[5]
|
||||
},
|
||||
|
||||
participantsPaneFooter: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
paddingBottom: BaseTheme.spacing[3],
|
||||
width: '100%'
|
||||
marginLeft: 'auto'
|
||||
},
|
||||
|
||||
inviteButton: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { FlatList, Platform, TextInput, View, ViewStyle } from 'react-native';
|
||||
import { FlatList, SafeAreaView, TextInput, View, ViewStyle } from 'react-native';
|
||||
import { Divider } from 'react-native-paper';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
@@ -97,9 +97,6 @@ const PollCreate = (props: AbstractProps) => {
|
||||
type = { TERTIARY } />
|
||||
);
|
||||
|
||||
const pollCreateButtonsContainerStyles = Platform.OS === 'android'
|
||||
? pollsStyles.pollCreateButtonsContainerAndroid : pollsStyles.pollCreateButtonsContainerIos;
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
const renderListItem = ({ index }: { index: number; }) => {
|
||||
|
||||
@@ -160,7 +157,7 @@ const PollCreate = (props: AbstractProps) => {
|
||||
), [ question ]);
|
||||
|
||||
return (
|
||||
<View style = { pollsStyles.pollCreateContainer as ViewStyle }>
|
||||
<SafeAreaView style = { pollsStyles.pollCreateContainer as ViewStyle }>
|
||||
<View style = { pollsStyles.pollCreateSubContainer as ViewStyle }>
|
||||
<FlatList
|
||||
ListHeaderComponent = { renderListHeaderComponent }
|
||||
@@ -169,7 +166,7 @@ const PollCreate = (props: AbstractProps) => {
|
||||
keyExtractor = { (item, index) => index.toString() }
|
||||
ref = { answerListRef }
|
||||
renderItem = { renderListItem } />
|
||||
<View style = { pollCreateButtonsContainerStyles as ViewStyle }>
|
||||
<View style = { pollsStyles.pollCreateButtonsContainer as ViewStyle }>
|
||||
<Button
|
||||
accessibilityLabel = 'polls.create.addOption'
|
||||
disabled = { answers.length >= ANSWERS_LIMIT }
|
||||
@@ -207,7 +204,7 @@ const PollCreate = (props: AbstractProps) => {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
@@ -42,28 +41,27 @@ const PollsPane = (props: AbstractProps) => {
|
||||
|
||||
}, [ isPollsTabFocused, unreadPollsCount ]);
|
||||
|
||||
const createPollButtonStyles = Platform.OS === 'android'
|
||||
? pollsStyles.createPollButtonAndroid : pollsStyles.createPollButtonIos;
|
||||
const renderCreateAPollBtn = useCallback(() => (
|
||||
!createMode && !isCreatePollsDisabled && <Button
|
||||
accessibilityLabel = 'polls.create.create'
|
||||
id = { t('polls.create.create') }
|
||||
labelKey = 'polls.create.create'
|
||||
onClick = { onCreate }
|
||||
style = { pollsStyles.createPollButton }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
), [ createMode, isCreatePollsDisabled ]);
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
contentContainerStyle = { pollsStyles.pollPane as StyleType }
|
||||
disableForcedKeyboardDismiss = { true }
|
||||
footerComponent = { renderCreateAPollBtn }
|
||||
hasExtraHeaderHeight = { true }
|
||||
style = { pollsStyles.pollPaneContainer as StyleType }>
|
||||
{
|
||||
createMode
|
||||
? <PollCreate setCreateMode = { setCreateMode } />
|
||||
: <>
|
||||
<PollsList setCreateMode = { setCreateMode } />
|
||||
{!isCreatePollsDisabled && <Button
|
||||
accessibilityLabel = 'polls.create.create'
|
||||
id = { t('polls.create.create') }
|
||||
labelKey = 'polls.create.create'
|
||||
onClick = { onCreate }
|
||||
style = { createPollButtonStyles }
|
||||
type = { BUTTON_TYPES.PRIMARY } />}
|
||||
</>
|
||||
: <PollsList setCreateMode = { setCreateMode } />
|
||||
}
|
||||
</JitsiScreen>
|
||||
);
|
||||
|
||||
@@ -158,13 +158,7 @@ export const pollsStyles = createStyleSheet({
|
||||
marginTop: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
pollCreateButtonsContainerAndroid: {
|
||||
marginBottom: BaseTheme.spacing[8],
|
||||
marginHorizontal: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
pollCreateButtonsContainerIos: {
|
||||
marginBottom: BaseTheme.spacing[5],
|
||||
pollCreateButtonsContainer: {
|
||||
marginHorizontal: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
@@ -210,14 +204,8 @@ export const pollsStyles = createStyleSheet({
|
||||
color: BaseTheme.palette.action01
|
||||
},
|
||||
|
||||
createPollButtonIos: {
|
||||
marginHorizontal: 20,
|
||||
marginVertical: BaseTheme.spacing[5]
|
||||
},
|
||||
|
||||
createPollButtonAndroid: {
|
||||
marginHorizontal: 20,
|
||||
marginVertical: BaseTheme.spacing[5]
|
||||
createPollButton: {
|
||||
marginHorizontal: BaseTheme.spacing[5],
|
||||
},
|
||||
|
||||
pollPane: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { ComponentType, PureComponent } from 'react';
|
||||
import { TouchableWithoutFeedback } from 'react-native';
|
||||
import { Edge, SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { TouchableWithoutFeedback, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
@@ -9,7 +8,6 @@ import { hideDialog } from '../../../base/dialog/actions';
|
||||
import { isDialogOpen } from '../../../base/dialog/functions';
|
||||
import { getParticipantCount } from '../../../base/participants/functions';
|
||||
import { StyleType } from '../../../base/styles/functions.native';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions.native';
|
||||
|
||||
import ReactionMenu from './ReactionMenu';
|
||||
|
||||
@@ -38,11 +36,6 @@ interface IProps {
|
||||
*/
|
||||
_styles: StyleType;
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the Toolbox is visible.
|
||||
*/
|
||||
_toolboxVisible: boolean;
|
||||
|
||||
/**
|
||||
* The width of the screen.
|
||||
*/
|
||||
@@ -87,13 +80,12 @@ class ReactionMenuDialog extends PureComponent<IProps> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const { _height, _participantCount, _styles, _toolboxVisible, _width } = this.props;
|
||||
const { _height, _participantCount, _styles, _width } = this.props;
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress = { this._onCancel }>
|
||||
<SafeAreaView
|
||||
edges = { [ 'bottom', 'left', 'right', !_toolboxVisible && 'top' ].filter(Boolean) as Edge[] }
|
||||
<View
|
||||
style = { [
|
||||
_styles,
|
||||
{
|
||||
@@ -103,7 +95,7 @@ class ReactionMenuDialog extends PureComponent<IProps> {
|
||||
<ReactionMenu
|
||||
onCancel = { this._onCancel }
|
||||
overflowMenu = { false } />
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
@@ -138,8 +130,7 @@ function _mapStateToProps(state: IReduxState) {
|
||||
_styles: ColorSchemeRegistry.get(state, 'Toolbox').reactionDialog,
|
||||
_width: state['features/base/responsive-ui'].clientWidth,
|
||||
_height: state['features/base/responsive-ui'].clientHeight,
|
||||
_participantCount: getParticipantCount(state),
|
||||
_toolboxVisible: isToolboxVisible(state)
|
||||
_participantCount: getParticipantCount(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -37,16 +37,25 @@ export function grantRecordingConsent() {
|
||||
|
||||
/**
|
||||
* Grants recording consent, unmutes audio/video, and closes the dialog.
|
||||
* Restores the mute state that existed before the consent dialog was shown,
|
||||
* respecting the user's intentional mute choices from prejoin or initial settings.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function grantRecordingConsentAndUnmute() {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
// Read the prejoin mute state from the dialog props stored in Redux
|
||||
const dialogProps = getState()['features/base/dialog'].componentProps as any;
|
||||
const audioMuted = dialogProps?.audioWasMuted ?? false;
|
||||
const videoMuted = dialogProps?.videoWasMuted ?? false;
|
||||
|
||||
batch(() => {
|
||||
dispatch(setAudioUnmutePermissions(false, true));
|
||||
dispatch(setVideoUnmutePermissions(false, true));
|
||||
dispatch(setAudioMuted(false, true));
|
||||
dispatch(setVideoMuted(false, VIDEO_MUTISM_AUTHORITY.USER, true));
|
||||
|
||||
// Restore to the mute state before consent was requested.
|
||||
dispatch(setAudioMuted(audioMuted, false));
|
||||
dispatch(setVideoMuted(videoMuted, VIDEO_MUTISM_AUTHORITY.USER, false));
|
||||
dispatch(hideDialog());
|
||||
});
|
||||
};
|
||||
|
||||
@@ -415,7 +415,7 @@ class AbstractStartRecordingDialog extends Component<IProps, IState> {
|
||||
|
||||
if (this.state.selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE
|
||||
&& this.state.shouldRecordTranscription) {
|
||||
dispatch(setRequestingSubtitles(true, _displaySubtitles, _subtitlesLanguage));
|
||||
dispatch(setRequestingSubtitles(true, _displaySubtitles, _subtitlesLanguage, true));
|
||||
} else {
|
||||
_conference?.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: this.state.shouldRecordTranscription
|
||||
|
||||
@@ -108,7 +108,8 @@ export default class AbstractStopRecordingDialog<P extends IProps>
|
||||
}
|
||||
|
||||
// TODO: this should be an action in transcribing. -saghul
|
||||
this.props.dispatch(setRequestingSubtitles(Boolean(_displaySubtitles), _displaySubtitles, _subtitlesLanguage));
|
||||
this.props.dispatch(
|
||||
setRequestingSubtitles(Boolean(_displaySubtitles), _displaySubtitles, _subtitlesLanguage, true));
|
||||
|
||||
this.props._conference?.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: false
|
||||
|
||||
@@ -8,20 +8,21 @@ import ConfirmDialog from '../../../../base/dialog/components/native/ConfirmDial
|
||||
import { setAudioMuted, setAudioUnmutePermissions, setVideoMuted, setVideoUnmutePermissions } from '../../../../base/media/actions';
|
||||
import { VIDEO_MUTISM_AUTHORITY } from '../../../../base/media/constants';
|
||||
import Link from '../../../../base/react/components/native/Link';
|
||||
import { IRecordingConsentDialogProps } from '../../../reducer';
|
||||
import styles from '../styles.native';
|
||||
|
||||
/**
|
||||
* Component that renders the dialog for explicit consent for recordings.
|
||||
*
|
||||
* @param {IRecordingConsentDialogProps} props - The component props.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function RecordingConsentDialog() {
|
||||
export default function RecordingConsentDialog({ audioWasMuted = false, videoWasMuted = false }: IRecordingConsentDialogProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { recordings } = useSelector((state: IReduxState) => state['features/base/config']);
|
||||
const { consentLearnMoreLink } = recordings ?? {};
|
||||
|
||||
|
||||
const consent = useCallback(() => {
|
||||
dispatch(setAudioUnmutePermissions(false, true));
|
||||
dispatch(setVideoUnmutePermissions(false, true));
|
||||
@@ -32,11 +33,13 @@ export default function RecordingConsentDialog() {
|
||||
const consentAndUnmute = useCallback(() => {
|
||||
dispatch(setAudioUnmutePermissions(false, true));
|
||||
dispatch(setVideoUnmutePermissions(false, true));
|
||||
dispatch(setAudioMuted(false, true));
|
||||
dispatch(setVideoMuted(false, VIDEO_MUTISM_AUTHORITY.USER, true));
|
||||
|
||||
// Restore to the mute state before consent was requested.
|
||||
dispatch(setAudioMuted(audioWasMuted, false));
|
||||
dispatch(setVideoMuted(videoWasMuted, VIDEO_MUTISM_AUTHORITY.USER, false));
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
}, [ audioWasMuted, videoWasMuted ]);
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
|
||||
@@ -10,6 +10,7 @@ import { grantRecordingConsent, grantRecordingConsentAndUnmute } from '../../../
|
||||
|
||||
/**
|
||||
* Component that renders the dialog for explicit consent for recordings.
|
||||
* The prejoin mute state is read from Redux by the action creator.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
|
||||
@@ -421,12 +421,21 @@ function _showExplicitConsentDialog(recorderSession: any, dispatch: IStore['disp
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture the current mute state BEFORE forcing mute for consent
|
||||
// This preserves the user's intentional mute choices from prejoin or initial settings
|
||||
const state = getState();
|
||||
const audioWasMuted = state['features/base/media'].audio.muted;
|
||||
const videoWasMuted = state['features/base/media'].video.muted;
|
||||
|
||||
batch(() => {
|
||||
dispatch(markConsentRequested(recorderSession.getID()));
|
||||
dispatch(setAudioUnmutePermissions(true, true));
|
||||
dispatch(setVideoUnmutePermissions(true, true));
|
||||
dispatch(setAudioMuted(true));
|
||||
dispatch(setVideoMuted(true));
|
||||
dispatch(openDialog('RecordingConsentDialog', RecordingConsentDialog));
|
||||
dispatch(openDialog('RecordingConsentDialog', RecordingConsentDialog, {
|
||||
audioWasMuted,
|
||||
videoWasMuted
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,6 +42,14 @@ export interface IRecordingState {
|
||||
wasStartRecordingSuggested?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the RecordingConsentDialog component.
|
||||
*/
|
||||
export interface IRecordingConsentDialogProps {
|
||||
audioWasMuted?: boolean;
|
||||
videoWasMuted?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the Redux store this feature stores its state in.
|
||||
*/
|
||||
|
||||
@@ -83,7 +83,7 @@ export function toggleRequestingSubtitles() {
|
||||
* @param {boolean} enabled - The new state of the subtitles.
|
||||
* @param {boolean} displaySubtitles - Whether to display subtitles or not.
|
||||
* @param {string} language - The language of the subtitles.
|
||||
* @param {boolean} backendRecordingOn - Whether backend recording is on.
|
||||
* @param {boolean} forceBackendRecordingOn - Whether to force that backend recording is on.
|
||||
* @returns {{
|
||||
* type: SET_REQUESTING_SUBTITLES,
|
||||
* backendRecordingOn: boolean,
|
||||
@@ -95,11 +95,13 @@ export function toggleRequestingSubtitles() {
|
||||
export function setRequestingSubtitles(
|
||||
enabled: boolean,
|
||||
displaySubtitles = true,
|
||||
language: string | null = `translation-languages:${DEFAULT_LANGUAGE}`) {
|
||||
language: string | null = `translation-languages:${DEFAULT_LANGUAGE}`,
|
||||
forceBackendRecordingOn: boolean = false) {
|
||||
return {
|
||||
type: SET_REQUESTING_SUBTITLES,
|
||||
displaySubtitles,
|
||||
enabled,
|
||||
forceBackendRecordingOn,
|
||||
language
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ const useStyles = makeStyles()(theme => {
|
||||
* Uses the same language options as LanguageSelectorDialog and
|
||||
* updates the subtitles language preference in Redux.
|
||||
*
|
||||
* @param {IProps} props - The component props.
|
||||
* @returns {JSX.Element} - The rendered component.
|
||||
*/
|
||||
function LanguageSelector() {
|
||||
@@ -51,6 +50,13 @@ function LanguageSelector() {
|
||||
state,
|
||||
selectedLanguage?.replace('translation-languages:', '')
|
||||
));
|
||||
const isAsyncTranscriptionEnabled = useSelector((state: IReduxState) =>
|
||||
state['features/base/conference'].conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription);
|
||||
|
||||
// Hide the "Translate to" option when asyncTranscription is enabled
|
||||
if (isAsyncTranscriptionEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps available languages to Select component options format.
|
||||
|
||||
@@ -98,7 +98,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
case SET_REQUESTING_SUBTITLES:
|
||||
_requestingSubtitlesChange(store, action.enabled, action.language);
|
||||
_requestingSubtitlesChange(store, action.enabled, action.language, action.forceBackendRecordingOn);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -344,13 +344,16 @@ function _getPrimaryLanguageCode(language: string) {
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {boolean} enabled - Whether subtitles should be enabled or not.
|
||||
* @param {string} language - The language to use for translation.
|
||||
* @param {boolean} forceBackendRecordingOn - Whether to force backend recording is on or not. This is used only when
|
||||
* we start recording, stopping is based on whether isTranscribingEnabled is already set.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _requestingSubtitlesChange(
|
||||
{ dispatch, getState }: IStore,
|
||||
enabled: boolean,
|
||||
language?: string | null) {
|
||||
language?: string | null,
|
||||
forceBackendRecordingOn: boolean = false) {
|
||||
const state = getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
const backendRecordingOn = conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription;
|
||||
@@ -375,7 +378,9 @@ function _requestingSubtitlesChange(
|
||||
}));
|
||||
dispatch(setSubtitlesError(true));
|
||||
});
|
||||
} else {
|
||||
}
|
||||
|
||||
if (backendRecordingOn || forceBackendRecordingOn) {
|
||||
conference?.getMetadataHandler()?.setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: true
|
||||
});
|
||||
@@ -388,7 +393,7 @@ function _requestingSubtitlesChange(
|
||||
language.replace('translation-languages:', ''));
|
||||
}
|
||||
|
||||
if (!enabled && backendRecordingOn
|
||||
if (!enabled && (backendRecordingOn || forceBackendRecordingOn)
|
||||
&& conference?.getMetadataHandler()?.getMetadata()[RECORDING_METADATA_ID]?.isTranscribingEnabled) {
|
||||
conference?.getMetadataHandler()?.setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: false
|
||||
|
||||
@@ -76,3 +76,13 @@ local function anonymous(self, message)
|
||||
end
|
||||
|
||||
sasl.registerMechanism("ANONYMOUS", {"anonymous"}, anonymous);
|
||||
|
||||
module:hook("pre-resource-unbind", function (e)
|
||||
local error, session = e.error, e.session;
|
||||
|
||||
prosody.events.fire_event('jitsi-pre-session-unbind', {
|
||||
jid = session.full_jid,
|
||||
session = session,
|
||||
error = error
|
||||
});
|
||||
end, 11);
|
||||
|
||||
@@ -61,6 +61,16 @@ end
|
||||
module:hook_global("bosh-session", init_session);
|
||||
module:hook_global("websocket-session", init_session);
|
||||
|
||||
module:hook("pre-resource-unbind", function (e)
|
||||
local error, session = e.error, e.session;
|
||||
|
||||
prosody.events.fire_event('jitsi-pre-session-unbind', {
|
||||
jid = session.full_jid,
|
||||
session = session,
|
||||
error = error
|
||||
});
|
||||
end, 11);
|
||||
|
||||
function provider.test_password(username, password)
|
||||
return nil, "Password based auth not supported";
|
||||
end
|
||||
|
||||
@@ -13,6 +13,7 @@ local inspect = require 'inspect';
|
||||
|
||||
local ban_check_count = module:measure("muc_auth_ban_check", "rate")
|
||||
local ban_check_users_banned_count = module:measure("muc_auth_ban_users_banned", "rate")
|
||||
local ban_check_error_count = module:measure("muc_auth_ban_check_error", "rate")
|
||||
|
||||
-- we will cache banned tokens to avoid extra requests
|
||||
-- on destroying session, websocket retries 2 more times before giving up
|
||||
@@ -68,6 +69,10 @@ local function shouldAllow(session)
|
||||
|
||||
cache:set(token, socket.gettime());
|
||||
end
|
||||
else
|
||||
ban_check_error_count();
|
||||
module:log("warn", "Error code:%s contacting url:%s response:%s request:%s content:%s",
|
||||
code, ACCESS_MANAGER_URL, response, request, content);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ end
|
||||
local jid_node = require 'util.jid'.node;
|
||||
local jid_host = require 'util.jid'.host;
|
||||
local jid_split = require 'util.jid'.split;
|
||||
local jid_resource = require 'util.jid'.resource;
|
||||
local jid_bare = require 'util.jid'.bare;
|
||||
local json = require 'cjson.safe';
|
||||
local st = require 'util.stanza';
|
||||
local uuid_gen = require 'util.uuid'.generate;
|
||||
@@ -65,12 +67,16 @@ local main_muc_service;
|
||||
-- Maps a breakout room jid to the main room jid
|
||||
local main_rooms_map = {};
|
||||
|
||||
-- Maps a full room JID to a bare connection jid for a participant that's changing rooms.
|
||||
local cache = require 'util.cache';
|
||||
local switching_room_cache = cache.new(1000);
|
||||
|
||||
-- Utility functions
|
||||
|
||||
function get_main_room_jid(room_jid)
|
||||
local _, host = jid_split(room_jid);
|
||||
|
||||
return
|
||||
return
|
||||
host == main_muc_component_config
|
||||
and room_jid
|
||||
or main_rooms_map[room_jid];
|
||||
@@ -391,7 +397,7 @@ function on_breakout_room_pre_create(event)
|
||||
end
|
||||
|
||||
function on_occupant_joined(event)
|
||||
local room = event.room;
|
||||
local occupant, room = event.occupant, event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
@@ -409,6 +415,9 @@ function on_occupant_joined(event)
|
||||
main_room.close_timer:stop();
|
||||
main_room.close_timer = nil;
|
||||
end
|
||||
|
||||
-- clear any switching state for this occupant, we always store main room / resource
|
||||
switching_room_cache:set(main_room_jid..'/'..jid_resource(occupant.nick), nil);
|
||||
end
|
||||
end
|
||||
|
||||
@@ -447,6 +456,11 @@ function on_occupant_pre_leave(event)
|
||||
prosody.events.fire_event('jitsi-breakout-occupant-leaving', {
|
||||
room = room; main_room = main_room; occupant = occupant; stanza = stanza; session = session;
|
||||
});
|
||||
|
||||
local presence_status = stanza:get_child_text('status');
|
||||
if presence_status == 'switch_room' then
|
||||
switching_room_cache:set(main_room.jid..'/'..jid_resource(occupant.nick), jid_bare(occupant.jid));
|
||||
end
|
||||
end
|
||||
|
||||
function on_occupant_left(event)
|
||||
@@ -518,6 +532,86 @@ function on_main_room_destroyed(event)
|
||||
end
|
||||
end
|
||||
|
||||
-- Checks for a conflict with a JID in the switching_room_cache. In case of a conflict sends an error and returns true (the join is not allowed).
|
||||
-- in switching_room_cache is the same as the jid that is sending the stanza, if that is the case we can allow
|
||||
-- the join to proceed by returning false. If there is no match we send an error and return true
|
||||
-- which should halt the join.
|
||||
-- @param jid - The jid to check, this is the jid requested to join breakout or main room
|
||||
-- @param from_bare_jid - The real jid of the occupant trying to join
|
||||
-- @param room - The room being joined
|
||||
-- @param stanza - The presence stanza
|
||||
-- @param origin - The session origin to send error if needed
|
||||
function check_switching_state(jid, from_bare_jid, room, stanza, origin)
|
||||
local switching_session_jid = switching_room_cache:get(jid);
|
||||
|
||||
if switching_session_jid and switching_session_jid ~= from_bare_jid then
|
||||
local reply = st.error_reply(stanza, "cancel", "conflict", nil, room.jid):up();
|
||||
origin.send(reply);
|
||||
return true;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function check_for_existing_occupant_in_room(room, requested_resource, bare_jid, stanza, origin)
|
||||
local dest_occupant = room:get_occupant_by_nick(room.jid..'/'..requested_resource);
|
||||
if dest_occupant ~= nil and bare_jid ~= jid_bare(dest_occupant.bare_jid) then
|
||||
origin.send(st.error_reply(stanza, 'cancel', 'conflict', nil, room.jid):up());
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
-- This is a request to join or change jid in main or breakout room. We need to check whether the requested jid does not
|
||||
-- conflict with a jid which is currently in switching state or already in another room.
|
||||
function on_occupant_pre_join_or_change(e)
|
||||
local room, stanza, origin = e.room, e.stanza, e.origin;
|
||||
local requested_jid = stanza.attr.to;
|
||||
|
||||
local main_room = get_main_room(room.jid);
|
||||
|
||||
-- case where the room can be destroyed while someone is switching to it
|
||||
if not main_room then
|
||||
origin.send(st.error_reply(stanza, 'cancel', 'service-unavailable'));
|
||||
return true;
|
||||
end
|
||||
|
||||
local main_room_requested_jid = main_room.jid..'/'..jid_resource(requested_jid);
|
||||
local bare_jid = jid_bare(stanza.attr.from);
|
||||
|
||||
-- we always store main room jid with resource in switching cache
|
||||
if check_switching_state(main_room_requested_jid, bare_jid, room, stanza, origin) then
|
||||
return true;
|
||||
end
|
||||
|
||||
if main_room == room then
|
||||
-- this is the main room we need to check all its breakout rooms
|
||||
for breakout_room_jid in pairs(room._data.breakout_rooms or {}) do
|
||||
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
|
||||
if breakout_room then
|
||||
if check_for_existing_occupant_in_room(
|
||||
breakout_room, jid_resource(requested_jid), bare_jid, stanza, origin) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
-- this is a breakout room let's check the main room
|
||||
if check_for_existing_occupant_in_room(main_room, jid_resource(requested_jid), bare_jid, stanza, origin) then
|
||||
return true;
|
||||
end
|
||||
|
||||
-- now let's check the rest of the breakout rooms
|
||||
for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
|
||||
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
|
||||
if breakout_room then
|
||||
if check_for_existing_occupant_in_room(
|
||||
breakout_room, jid_resource(requested_jid), bare_jid, stanza, origin) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Module operations
|
||||
|
||||
@@ -544,6 +638,8 @@ function process_breakout_rooms_muc_loaded(breakout_rooms_muc, host_module)
|
||||
host_module:hook('muc-occupant-left', on_occupant_left);
|
||||
host_module:hook('muc-room-pre-create', on_breakout_room_pre_create);
|
||||
host_module:hook('muc-occupant-pre-leave', on_occupant_pre_leave);
|
||||
host_module:hook('muc-occupant-pre-join', on_occupant_pre_join_or_change);
|
||||
host_module:hook('muc-occupant-pre-change', on_occupant_pre_join_or_change);
|
||||
|
||||
host_module:hook('muc-disco#info', function (event)
|
||||
local room = event.room;
|
||||
@@ -641,6 +737,24 @@ process_host_module(breakout_rooms_muc_component_config, function(host_module, h
|
||||
end
|
||||
end);
|
||||
|
||||
-- clears switching_room_cache on resource unbind (disconnect one way or another)
|
||||
local function handle_pre_resource_unbind(event)
|
||||
local participant_bare_jid = jid_bare(event.jid);
|
||||
|
||||
-- check switching_room_cache for anyone that is switching rooms but got disconnected so we can clean up the map
|
||||
local keysToRemove = {};
|
||||
for key, value in switching_room_cache:items() do
|
||||
if value == participant_bare_jid then
|
||||
table.insert(keysToRemove, key)
|
||||
end
|
||||
end
|
||||
|
||||
for _, key in ipairs(keysToRemove) do
|
||||
switching_room_cache:set(key, nil);
|
||||
end
|
||||
end
|
||||
module:hook_global('jitsi-pre-session-unbind', handle_pre_resource_unbind);
|
||||
|
||||
-- operates on already loaded main muc module
|
||||
function process_main_muc_loaded(main_muc, host_module)
|
||||
module:log('debug', 'Main muc loaded');
|
||||
@@ -650,6 +764,18 @@ function process_main_muc_loaded(main_muc, host_module)
|
||||
host_module:hook('muc-occupant-joined', on_occupant_joined);
|
||||
host_module:hook('muc-occupant-left', on_occupant_left);
|
||||
host_module:hook('muc-room-destroyed', on_main_room_destroyed, 1); -- prosody handles it at 0
|
||||
|
||||
host_module:hook('muc-occupant-pre-leave', function(event)
|
||||
local room, occupant, session, stanza = event.room, event.occupant, event.origin, event.stanza;
|
||||
|
||||
local presence_status = stanza:get_child_text('status');
|
||||
if presence_status == 'switch_room' then
|
||||
switching_room_cache:set(occupant.nick, jid_bare(occupant.jid));
|
||||
end
|
||||
end);
|
||||
|
||||
host_module:hook('muc-occupant-pre-join', on_occupant_pre_join_or_change);
|
||||
host_module:hook('muc-occupant-pre-change', on_occupant_pre_join_or_change);
|
||||
end
|
||||
|
||||
-- process or waits to process the main muc component
|
||||
|
||||
@@ -2,6 +2,7 @@ import BasePageObject from './BasePageObject';
|
||||
|
||||
const ASK_TO_UNMUTE_NOTIFICATION_ID = 'notify.hostAskedUnmute';
|
||||
const AV_MODERATION_MUTED_NOTIFICATION_ID = 'notify.moderationInEffectTitle';
|
||||
const AV_MODERATION_VIDEO_MUTED_NOTIFICATION_ID = 'notify.moderationInEffectVideoTitle';
|
||||
const JOIN_MULTIPLE_TEST_ID = 'notify.connectedThreePlusMembers';
|
||||
const JOIN_ONE_TEST_ID = 'notify.connectedOneMember';
|
||||
const JOIN_TWO_TEST_ID = 'notify.connectedTwoMembers';
|
||||
@@ -57,6 +58,13 @@ export default class Notifications extends BasePageObject {
|
||||
return this.closeNotification(AV_MODERATION_MUTED_NOTIFICATION_ID, skipNonExisting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the video muted notification.
|
||||
*/
|
||||
async closeAVModerationVideoMutedNotification(skipNonExisting = false) {
|
||||
return this.closeNotification(AV_MODERATION_VIDEO_MUTED_NOTIFICATION_ID, skipNonExisting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the ask to unmute notification.
|
||||
*/
|
||||
@@ -106,14 +114,22 @@ export default class Notifications extends BasePageObject {
|
||||
* @private
|
||||
*/
|
||||
private async closeNotification(testId: string, skipNonExisting = false) {
|
||||
const closeButton = this.participant.driver.$('[data-testid="${testId}"] #close-notification');
|
||||
const closeButton = this.participant.driver.$(`[data-testid="${testId}"] #close-notification`);
|
||||
|
||||
if (skipNonExisting && !await closeButton.isExisting()) {
|
||||
return Promise.resolve();
|
||||
try {
|
||||
if (skipNonExisting && !await closeButton.isExisting()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
await closeButton.moveTo();
|
||||
await closeButton.click();
|
||||
} catch (e) {
|
||||
console.error(`Error closing notification ${testId}`, e);
|
||||
|
||||
if (!skipNonExisting) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
await closeButton.moveTo();
|
||||
await closeButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,7 +40,7 @@ export default class PasswordDialog extends BaseDialog {
|
||||
const passwordInput = this.participant.driver.$(INPUT_KEY_XPATH);
|
||||
|
||||
await passwordInput.waitForExist();
|
||||
await passwordInput.waitForClickable({ timeout: 2000 });
|
||||
await passwordInput.waitForStable();
|
||||
await passwordInput.click();
|
||||
await passwordInput.clearValue();
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ export async function unmuteAudioAndCheck(testee: Participant, observer: Partici
|
||||
* @param observer
|
||||
*/
|
||||
export async function unmuteVideoAndCheck(testee: Participant, observer: Participant): Promise<void> {
|
||||
await testee.getNotifications().closeAskToUnmuteNotification(true);
|
||||
await testee.getNotifications().closeAVModerationVideoMutedNotification(true);
|
||||
await testee.getToolbar().clickVideoUnmuteButton();
|
||||
|
||||
await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
|
||||
|
||||
@@ -122,12 +122,12 @@ describe('Audio/video moderation', () => {
|
||||
await moderatorParticipantsPane.assertVideoMuteIconIsDisplayed(moderator);
|
||||
await nonModeratorParticipantsPane.assertVideoMuteIconIsDisplayed(nonModerator);
|
||||
|
||||
await moderatorParticipantsPane.allowVideo(nonModerator);
|
||||
await moderatorParticipantsPane.askToUnmute(nonModerator, false);
|
||||
|
||||
await nonModerator.getNotifications().waitForAskToUnmuteNotification();
|
||||
|
||||
await unmuteAudioAndCheck(nonModerator, p1);
|
||||
|
||||
await moderatorParticipantsPane.allowVideo(nonModerator);
|
||||
await nonModerator.getNotifications().waitForAskToUnmuteNotification();
|
||||
await unmuteVideoAndCheck(nonModerator, p1);
|
||||
|
||||
await moderatorParticipantsPane.clickContextMenuButton();
|
||||
@@ -190,6 +190,10 @@ describe('Audio/video moderation', () => {
|
||||
// stop video and check
|
||||
await p1.getFilmstrip().muteVideo(p2);
|
||||
|
||||
// close and open participants pane to make sure the context menu disappears
|
||||
await p1.getParticipantsPane().close();
|
||||
await p1.getParticipantsPane().open();
|
||||
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
|
||||
@@ -256,11 +260,12 @@ async function unmuteByModerator(
|
||||
await moderator.getNotifications().waitForRaisedHandNotification();
|
||||
|
||||
// ask participant to unmute
|
||||
await moderatorParticipantsPane.allowVideo(participant);
|
||||
await moderatorParticipantsPane.askToUnmute(participant, false);
|
||||
await participant.getNotifications().waitForAskToUnmuteNotification();
|
||||
|
||||
await unmuteAudioAndCheck(participant, moderator);
|
||||
|
||||
await moderatorParticipantsPane.allowVideo(participant);
|
||||
await participant.getNotifications().waitForAskToUnmuteNotification();
|
||||
await unmuteVideoAndCheck(participant, moderator);
|
||||
|
||||
if (stopModeration) {
|
||||
|
||||
@@ -194,6 +194,8 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
} ]
|
||||
],
|
||||
|
||||
execArgv: [ '--stack-trace-limit=100' ],
|
||||
|
||||
// =====
|
||||
// Hooks
|
||||
// =====
|
||||
|
||||
Reference in New Issue
Block a user