Compare commits

...

12 Commits

Author SHA1 Message Date
Calin-Teodor
3db92da369 chore(sdks, version): bump to 12.0.0 2026-01-22 14:02:25 +02:00
Calin-Teodor
a63736a42e chore(apps, version): bump to 26.0.0 2026-01-22 14:02:25 +02:00
Calin-Teodor
79fca7f0bc feat(breakout-rooms): fix margin for add button 2026-01-22 14:01:29 +02:00
Calin-Teodor
fc170891cb feat(subtitles): rework how isAsyncTranscriptionEnabled is being used 2026-01-22 14:00:06 +02:00
Calin-Teodor
775cc52f66 feat(subtitles): hide translation UI for both web and native 2026-01-22 10:50:03 +02:00
Calin-Teodor
3baede6ff1 feat(config): add reducedUIEnabled config to control web side 2026-01-22 10:44:15 +02:00
Дамян Минков
9462a9ce36 feat(breakout-rooms): Adds some jid validation on joining. (#16858)
* feat(breakout-rooms): Adds some jid validation on joining.

* squash: Fix comments.

* Update resources/prosody-plugins/mod_muc_breakout_rooms.lua

Co-authored-by: bgrozev <boris@jitsi.org>

* Update resources/prosody-plugins/mod_muc_breakout_rooms.lua

Co-authored-by: bgrozev <boris@jitsi.org>

---------

Co-authored-by: bgrozev <boris@jitsi.org>
2026-01-21 19:53:27 -06:00
Aaron van Meerten
4713062200 feat(muc_auth_ban): add metrics and logs for error responses from access backend (#16853) 2026-01-21 13:47:32 +01:00
Calinteodor
65eb2a2899 feat(base/modal): small updates around JitsiScreen footer (#16727)
*UI updates and reworks around JitsiScreen footer and its children.
2026-01-21 13:05:05 +02:00
Boris Grozev
f0452d05b9 feat: Hide the translation UI when asyncTranscription is used. 2026-01-20 11:56:13 -06:00
damencho
1102f4205a fix: Fixes setting backend recording when async transcriptions are not on. 2026-01-20 11:56:06 -06:00
Calinteodor
447def54c8 feat(chat/native): add Closed Captions tab (#16787)
* Added CC tab inside Chat screen and some UI fixes on mobile.
2026-01-20 13:33:46 +02:00
57 changed files with 5115 additions and 497 deletions

View File

@@ -32,5 +32,5 @@ newArchEnabled=false
# Use this property to enable or disable the Hermes JS engine.
hermesEnabled=true
appVersion=99.0.0
sdkVersion=0.0.0
appVersion=26.0.0
sdkVersion=12.0.0

View File

@@ -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.

View File

@@ -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

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>99.0.0</string>
<string>26.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>99.0.0</string>
<string>26.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>99.0.0</string>
<string>12.0.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>99.0.0</string>
<string>12.0.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>

View File

@@ -790,7 +790,7 @@ function initCommands() {
}
if (transcription) {
APP.store.dispatch(setRequestingSubtitles(true, false, null));
APP.store.dispatch(setRequestingSubtitles(true, false, null, true));
}
},

14
package-lock.json generated
View File

@@ -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",
@@ -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": "*",
@@ -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",

View File

@@ -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",

3988
react-native-sdk/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@jitsi/react-native-sdk",
"version": "0.0.0",
"version": "12.0.0",
"description": "React Native SDK for Jitsi Meet.",
"main": "index.tsx",
"license": "Apache-2.0",
@@ -11,85 +11,83 @@
"url": "git+https://github.com/jitsi/jitsi-meet.git"
},
"dependencies": {
"@braintree/sanitize-url": "0.0.0",
"@jitsi/js-utils": "0.0.0",
"@jitsi/logger": "0.0.0",
"@jitsi/rtcstats": "0.0.0",
"@react-navigation/bottom-tabs": "0.0.0",
"@react-navigation/elements": "0.0.0",
"@react-navigation/material-top-tabs": "0.0.0",
"@react-navigation/native": "0.0.0",
"@react-navigation/stack": "0.0.0",
"@stomp/stompjs": "0.0.0",
"@xmldom/xmldom": "0.0.0",
"abab": "0.0.0",
"base64-js": "0.0.0",
"dayjs": "0.0.0",
"grapheme-splitter": "0.0.0",
"i18n-iso-countries": "0.0.0",
"i18next": "0.0.0",
"js-md5": "0.0.0",
"i18next-http-backend": "0.0.0",
"js-sha512": "0.0.0",
"jwt-decode": "0.0.0",
"lib-jitsi-meet": "0.0.0",
"lodash-es": "0.0.0",
"optional-require": "0.0.0",
"promise.allsettled": "0.0.0",
"promise.withresolvers": "0.0.0",
"punycode": "0.0.0",
"react-emoji-render": "0.0.0",
"react-i18next": "0.0.0",
"react-linkify": "0.0.0",
"react-native-dialog": "0.0.0",
"react-native-paper": "0.0.0",
"react-native-svg-transformer": "0.0.0",
"react-native-tab-view": "0.0.0",
"react-native-url-polyfill": "0.0.0",
"react-native-youtube-iframe": "0.0.0",
"react-redux": "0.0.0",
"redux": "0.0.0",
"redux-thunk": "0.0.0",
"text-encoding": "0.0.0",
"unorm": "0.0.0",
"util": "0.0.0",
"uuid": "0.0.0",
"zxcvbn": "0.0.0"
"@braintree/sanitize-url": "7.0.0",
"@jitsi/js-utils": "2.6.7",
"@jitsi/logger": "2.1.1",
"@react-navigation/bottom-tabs": "6.6.0",
"@react-navigation/elements": "1.3.30",
"@react-navigation/material-top-tabs": "6.6.13",
"@react-navigation/native": "6.1.17",
"@react-navigation/stack": "6.4.0",
"@stomp/stompjs": "7.0.0",
"@xmldom/xmldom": "0.8.7",
"abab": "2.0.6",
"base64-js": "1.5.1",
"dayjs": "1.11.13",
"grapheme-splitter": "1.0.4",
"i18n-iso-countries": "6.8.0",
"i18next": "17.0.6",
"js-md5": "0.6.1",
"i18next-http-backend": "2.2.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",
"lodash-es": "4.17.21",
"optional-require": "1.0.3",
"promise.withresolvers": "1.0.3",
"punycode": "2.3.0",
"react-emoji-render": "2.0.1",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-paper": "5.10.3",
"react-native-svg-transformer": "1.2.0",
"react-native-tab-view": "3.5.2",
"react-native-url-polyfill": "2.0.0",
"react-native-youtube-iframe": "2.3.0",
"react-redux": "7.2.9",
"redux": "4.0.4",
"redux-thunk": "2.4.1",
"text-encoding": "0.7.0",
"unorm": "1.6.0",
"util": "0.12.1",
"uuid": "8.3.2",
"zxcvbn": "4.4.2"
},
"peerDependencies": {
"@amplitude/analytics-react-native": "0.0.0",
"@giphy/react-native-sdk": "0.0.0",
"@react-native-async-storage/async-storage": "0.0.0",
"@react-native-clipboard/clipboard": "0.0.0",
"@react-native-community/netinfo": "0.0.0",
"@react-native-community/slider": "0.0.0",
"@react-native-google-signin/google-signin": "0.0.0",
"@sayem314/react-native-keep-awake": "0.0.0",
"react-native": "0.0.0",
"@amplitude/analytics-react-native": "1.4.13",
"@giphy/react-native-sdk": "4.1.0",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-clipboard/clipboard": "1.14.3",
"@react-native-community/netinfo": "11.1.0",
"@react-native-community/slider": "4.5.6",
"@react-native-google-signin/google-signin": "10.1.0",
"@sayem314/react-native-keep-awake": "1.3.1",
"react-native": "~0.77.0",
"react": "*",
"react-native-background-timer": "0.0.0",
"react-native-calendar-events": "0.0.0",
"react-native-default-preference": "0.0.0",
"react-native-device-info": "0.0.0",
"react-native-get-random-values": "0.0.0",
"react-native-gesture-handler": "0.0.0",
"react-native-pager-view": "0.0.0",
"react-native-performance": "0.0.0",
"react-native-orientation-locker": "0.0.0",
"react-native-safe-area-context": "0.0.0",
"react-native-screens": "0.0.0",
"react-native-sound": "0.0.0",
"react-native-splash-view": "0.0.0",
"react-native-svg": "0.0.0",
"react-native-video": "0.0.0",
"react-native-background-timer": "https://github.com/jitsi/react-native-background-timer.git#d180dfaa4486ae3ee17d01242db92cb3195f4718",
"react-native-calendar-events": "https://github.com/jitsi/react-native-calendar-events.git#47f068dedfed7c0f72042e093f688eb11624eb7b",
"react-native-default-preference": "https://github.com/jitsi/react-native-default-preference.git#c9bf63bdc058e3fa2aa0b87b1ee1af240f44ed02",
"react-native-device-info": "12.1.0",
"react-native-get-random-values": "1.11.0",
"react-native-gesture-handler": "2.24.0",
"react-native-pager-view": "6.8.1",
"react-native-performance": "5.1.2",
"react-native-orientation-locker": "https://github.com/jitsi/react-native-orientation-locker.git#fe095651d819cf134624f786b61fc8667862178a",
"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",
"react-native-svg": "15.11.2",
"react-native-video": "6.13.0",
"react-native-watch-connectivity": "0.0.0",
"react-native-webrtc": "0.0.0",
"react-native-webview": "0.0.0",
"react-native-worklets-core": "0.0.0"
"react-native-webrtc": "124.0.7",
"react-native-webview": "13.13.5",
"react-native-worklets-core": "https://github.com/jitsi/react-native-worklets-core.git#8c5dfab2a5907305da8971696a781b60f0f9cb18"
},
"devDependencies": {
"@babel/plugin-proposal-nullish-coalescing-operator": "0.0.0",
"@babel/plugin-proposal-optional-chaining": "0.0.0"
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-optional-chaining": "7.21.0"
},
"scripts": {
"postinstall": "node sdk_instructions.js",
@@ -101,4 +99,4 @@
"keywords": [
"react-native"
]
}
}

View File

@@ -559,6 +559,7 @@ export interface IConfig {
skipConsentInMeeting?: boolean;
suggestRecording?: boolean;
};
reducedUIEnabled?: boolean;
reducedUImainToolbarButtons?: Array<string>;
remoteVideoMenu?: {
disableDemote?: boolean;

View File

@@ -215,6 +215,7 @@ export default [
'recordings.showPrejoinWarning',
'recordings.showRecordingLink',
'recordings.suggestRecording',
'reducedUIEnabled',
'reducedUImainToolbarButtons',
'replaceParticipant',
'resolution',

View File

@@ -17,7 +17,7 @@ interface IProps {
/**
* The children component(s) of the Modal, to be rendered.
*/
children: React.ReactNode;
children?: React.ReactNode;
/**
* Additional style to be appended to the KeyboardAvoidingView content container.
@@ -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>
);

View File

@@ -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(() => {

View File

@@ -8,7 +8,7 @@ export default {
button: {
marginBottom: BaseTheme.spacing[4],
marginHorizontal: BaseTheme.spacing[2]
marginHorizontal: BaseTheme.spacing[3]
},
collapsibleList: {

View File

@@ -22,7 +22,7 @@ export function openChat(participant?: IParticipant | undefined | Object, disabl
if (disablePolls) {
navigate(screen.conference.chat);
} else {
navigate(screen.conference.chatandpolls.main);
navigate(screen.conference.chatTabs.main);
}
dispatch(setFocusedTab(ChatTabs.CHAT));

View File

@@ -0,0 +1,111 @@
import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../app/types';
import { openDialog } from '../../base/dialog/actions';
import { IMessageGroup, groupMessagesBySender } from '../../base/util/messageGrouping';
// @ts-ignore
import { StartRecordingDialog } from '../../recording/components/Recording';
import { setRequestingSubtitles } from '../../subtitles/actions.any';
import { canStartSubtitles } from '../../subtitles/functions.any';
import { ISubtitle } from '../../subtitles/types';
import { isTranscribing } from '../../transcribing/functions';
export type AbstractProps = {
canStartSubtitles: boolean;
filteredSubtitles: ISubtitle[];
groupedSubtitles: IMessageGroup<ISubtitle>[];
isButtonPressed: boolean;
isTranscribing: boolean;
startClosedCaptions: () => void;
};
const AbstractClosedCaptions = (Component: ComponentType<AbstractProps>) => () => {
const dispatch = useDispatch();
const subtitles = useSelector((state: IReduxState) => state['features/subtitles'].subtitlesHistory);
const language = useSelector((state: IReduxState) => state['features/subtitles']._language);
const selectedLanguage = language?.replace('translation-languages:', '');
const _isTranscribing = useSelector(isTranscribing);
const _canStartSubtitles = useSelector(canStartSubtitles);
const [ isButtonPressed, setButtonPressed ] = useState(false);
const subtitlesError = useSelector((state: IReduxState) => state['features/subtitles']._hasError);
const isAsyncTranscriptionEnabled = useSelector((state: IReduxState) =>
state['features/base/conference'].conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription);
const filteredSubtitles = useMemo(() => {
// First, create a map of transcription messages by message ID
const transcriptionMessages = new Map(
subtitles
.filter(s => s.isTranscription)
.map(s => [ s.id, s ])
);
if (!selectedLanguage) {
// When no language is selected, show all original transcriptions
return Array.from(transcriptionMessages.values());
}
// Then, create a map of translation messages by message ID
const translationMessages = new Map(
subtitles
.filter(s => !s.isTranscription && s.language === selectedLanguage)
.map(s => [ s.id, s ])
);
// When a language is selected, for each transcription message:
// 1. Use its translation if available
// 2. Fall back to the original transcription if no translation exists
return Array.from(transcriptionMessages.values())
.filter((m: ISubtitle) => !m.interim)
.map(m => translationMessages.get(m.id) ?? m);
}, [ subtitles, selectedLanguage ]);
const groupedSubtitles = useMemo(() =>
groupMessagesBySender(filteredSubtitles), [ filteredSubtitles ]);
const startClosedCaptions = useCallback(() => {
if (isAsyncTranscriptionEnabled) {
dispatch(openDialog('StartRecordingDialog', StartRecordingDialog, {
recordAudioAndVideo: false
}));
} else {
if (isButtonPressed) {
return;
}
dispatch(setRequestingSubtitles(true, false, null));
setButtonPressed(true);
}
}, [ isAsyncTranscriptionEnabled, dispatch, isButtonPressed, openDialog, setButtonPressed ]);
useEffect(() => {
if (subtitlesError && isButtonPressed && !isAsyncTranscriptionEnabled) {
setButtonPressed(false);
}
}, [ subtitlesError, isButtonPressed, isAsyncTranscriptionEnabled ]);
useEffect(() => {
if (!_isTranscribing && isButtonPressed && !isAsyncTranscriptionEnabled) {
setButtonPressed(false);
}
}, [ _isTranscribing, isButtonPressed, isAsyncTranscriptionEnabled ]);
useEffect(() => {
if (isButtonPressed && !isAsyncTranscriptionEnabled) {
setButtonPressed(false);
}
}, [ isButtonPressed, isAsyncTranscriptionEnabled ]);
return (
<Component
canStartSubtitles = { _canStartSubtitles }
filteredSubtitles = { filteredSubtitles }
groupedSubtitles = { groupedSubtitles }
isButtonPressed = { isButtonPressed }
isTranscribing = { _isTranscribing }
startClosedCaptions = { startClosedCaptions } />
);
};
export default AbstractClosedCaptions;

View File

@@ -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 } />

View File

@@ -43,7 +43,7 @@ class ChatButton extends AbstractButton<IProps> {
override _handleClick() {
this.props._isPollsDisabled
? navigate(screen.conference.chat)
: navigate(screen.conference.chatandpolls.main);
: navigate(screen.conference.chatTabs.main);
}
/**

View File

@@ -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>
);

View File

@@ -0,0 +1,130 @@
import { useNavigation } from '@react-navigation/native';
import React, { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { TouchableHighlight, View, ViewStyle } from 'react-native';
import { Text } from 'react-native-paper';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import Icon from '../../../base/icons/components/Icon';
import { IconArrowRight, IconSubtitles } from '../../../base/icons/svg';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { StyleType } from '../../../base/styles/functions.any';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { TabBarLabelCounter } from '../../../mobile/navigation/components/TabBarLabelCounter';
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { ChatTabs } from '../../constants';
import AbstractClosedCaptions, { AbstractProps } from '../AbstractClosedCaptions';
import { SubtitlesMessagesContainer } from './SubtitlesMessagesContainer';
import { closedCaptionsStyles } from './styles';
/**
* Component that displays the closed captions interface.
*
* @returns {JSX.Element} - The ClosedCaptions component.
*/
const ClosedCaptions = ({
canStartSubtitles,
filteredSubtitles,
groupedSubtitles,
isButtonPressed,
isTranscribing,
startClosedCaptions
}: AbstractProps): JSX.Element => {
const navigation = useNavigation();
const { t } = useTranslation();
const isCCTabFocused = useSelector((state: IReduxState) => state['features/chat'].focusedTab === ChatTabs.CLOSED_CAPTIONS);
const selectedLanguage = useSelector((state: IReduxState) => state['features/subtitles']._language);
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({
tabBarLabel: () => (
<TabBarLabelCounter
isFocused = { isCCTabFocused }
label = { t('chat.tabs.closedCaptions') } />
)
});
}, [ isCCTabFocused, navigation, t ]);
const getContentContainerStyle = () => {
if (isTranscribing) {
return closedCaptionsStyles.transcribingContainer as StyleType;
}
return closedCaptionsStyles.emptyContentContainer as StyleType;
};
const renderContent = () => {
if (!isTranscribing) {
if (canStartSubtitles) {
return (
<View style = { closedCaptionsStyles.emptyContent as ViewStyle }>
<Button
accessibilityLabel = { t('closedCaptionsTab.startClosedCaptionsButton') }
disabled = { isButtonPressed }
labelKey = 'closedCaptionsTab.startClosedCaptionsButton'
onClick = { startClosedCaptions }
type = { BUTTON_TYPES.PRIMARY } />
</View>
);
}
return (
<View style = { closedCaptionsStyles.emptyContent as ViewStyle }>
<Icon
color = { BaseTheme.palette.icon03 }
size = { 100 }
src = { IconSubtitles } />
<Text style = { [ closedCaptionsStyles.emptyStateText, { marginTop: BaseTheme.spacing[3] } ] }>
{ t('closedCaptionsTab.emptyState') }
</Text>
</View>
);
}
return (
<>
{
// 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 }
messages = { filteredSubtitles } />
</View>
</>
);
};
return (
<JitsiScreen
contentContainerStyle = { getContentContainerStyle() }
disableForcedKeyboardDismiss = { true }
hasExtraHeaderHeight = { true }
style = { closedCaptionsStyles.container as StyleType }>
{ renderContent() }
</JitsiScreen>
);
};
export default AbstractClosedCaptions(ClosedCaptions);

View File

@@ -44,19 +44,23 @@ class MessageContainer extends Component<IProps, any> {
*/
override render() {
const data = this._getMessagesGroupedBySender();
const noMessages = data.length === 0;
return (
<FlatList
ListEmptyComponent = { this._renderListEmptyComponent }
bounces = { false }
data = { data }
// @ts-ignore
contentContainerStyle = { noMessages && styles.emptyListContentContainer }
data = { data }
// Workaround for RN bug:
// https://github.com/facebook/react-native/issues/21196
inverted = { Boolean(data.length) }
keyExtractor = { this._keyExtractor }
keyboardShouldPersistTaps = 'handled'
renderItem = { this._renderMessageGroup } />
renderItem = { this._renderMessageGroup }
style = { noMessages && styles.emptyListStyle } />
);
}

View File

@@ -67,8 +67,8 @@ class PrivateMessageButton extends AbstractButton<IProps, any> {
? navigate(screen.conference.chat, {
privateMessageRecipient: this.props._participant
})
: navigate(screen.conference.chatandpolls.main, {
screen: screen.conference.chatandpolls.tab.chat,
: navigate(screen.conference.chatTabs.main, {
screen: screen.conference.chatTabs.tab.chat,
params: {
privateMessageRecipient: this.props._participant
}

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { Text, View, ViewStyle } from 'react-native';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { getParticipantDisplayName } from '../../../base/participants/functions';
import { ISubtitle } from '../../../subtitles/types';
import { closedCaptionsStyles } from './styles';
interface IProps extends ISubtitle {
showDisplayName: boolean;
}
export default function SubtitleMessage({ participantId, text, timestamp, interim, showDisplayName }: IProps) {
const participantName = useSelector((state: IReduxState) =>
getParticipantDisplayName(state, participantId));
const containerStyle: ViewStyle[] = [
closedCaptionsStyles.subtitleMessageContainer as ViewStyle
];
if (interim) {
containerStyle.push(closedCaptionsStyles.subtitleMessageInterim as ViewStyle);
}
return (
<View style = { containerStyle }>
<View style = { closedCaptionsStyles.subtitleMessageContent as ViewStyle }>
{
showDisplayName && (
<Text style = { closedCaptionsStyles.subtitleMessageHeader }>
{ participantName }
</Text>
)
}
<Text style = { closedCaptionsStyles.subtitleMessageText }>{ text }</Text>
<Text style = { closedCaptionsStyles.subtitleMessageTimestamp }>
{ new Date(timestamp).toLocaleTimeString() }
</Text>
</View>
</View>
);
}

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { View, ViewStyle } from 'react-native';
import Avatar from '../../../base/avatar/components/Avatar';
import { ISubtitle } from '../../../subtitles/types';
import SubtitleMessage from './SubtitleMessage';
import { closedCaptionsStyles } from './styles';
interface IProps {
messages: ISubtitle[];
senderId: string;
}
export function SubtitlesGroup({ messages, senderId }: IProps) {
if (!messages.length) {
return null;
}
return (
<View style = { closedCaptionsStyles.subtitlesGroupContainer as ViewStyle }>
<View style = { closedCaptionsStyles.subtitlesGroupAvatar as ViewStyle }>
<Avatar
participantId = { senderId }
size = { 32 } />
</View>
<View style = { closedCaptionsStyles.subtitlesGroupMessagesContainer as ViewStyle }>
{
messages.map((message, index) => (
<SubtitleMessage
key = { `${message.timestamp}-${message.id}` }
showDisplayName = { index === 0 }
{ ...message } />
))
}
</View>
</View>
);
}

View File

@@ -0,0 +1,105 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { NativeScrollEvent, NativeSyntheticEvent, ScrollView, View, ViewStyle } from 'react-native';
import Icon from '../../../base/icons/components/Icon';
import { IconArrowDown } from '../../../base/icons/svg';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { ISubtitle } from '../../../subtitles/types';
import { SubtitlesGroup } from './SubtitlesGroup';
import { closedCaptionsStyles } from './styles';
/**
* The threshold value used to determine if the user is at the bottom of the scroll view.
*/
const SCROLL_THRESHOLD = 50;
interface IProps {
groups: Array<{
messages: ISubtitle[];
senderId: string;
}>;
messages: ISubtitle[];
}
export function SubtitlesMessagesContainer({ messages, groups }: IProps) {
const [ hasNewMessages, setHasNewMessages ] = useState(false);
const [ isScrolledToBottom, setIsScrolledToBottom ] = useState(true);
const scrollViewRef = useRef<ScrollView>(null);
const previousMessages = useRef(messages);
const scrollToBottom = useCallback((withAnimation: boolean) => {
scrollViewRef.current?.scrollToEnd({ animated: withAnimation });
}, []);
const handleNewMessagesClick = useCallback(() => {
scrollToBottom(true);
}, [ scrollToBottom ]);
const handleScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
const isAtBottom = contentOffset.y + layoutMeasurement.height >= contentSize.height - SCROLL_THRESHOLD;
setIsScrolledToBottom(isAtBottom);
if (isAtBottom) {
setHasNewMessages(false);
}
}, []);
useEffect(() => {
scrollToBottom(false);
}, [ scrollToBottom ]);
useEffect(() => {
const newMessages = messages.filter(message => !previousMessages.current.includes(message));
if (newMessages.length > 0) {
if (isScrolledToBottom) {
scrollToBottom(false);
} else {
setHasNewMessages(true);
}
}
previousMessages.current = messages;
}, [ messages, scrollToBottom ]);
return (
<View style = { closedCaptionsStyles.subtitlesMessagesContainer as ViewStyle }>
<ScrollView
contentContainerStyle = { closedCaptionsStyles.subtitlesMessagesList as ViewStyle }
onScroll = { handleScroll }
ref = { scrollViewRef }
scrollEventThrottle = { 16 }>
{
groups.map(group => (
<SubtitlesGroup
key = { `${group.senderId}-${group.messages[0]?.timestamp}` }
messages = { group.messages }
senderId = { group.senderId } />
))
}
</ScrollView>
{
!isScrolledToBottom && hasNewMessages && (
<View style = { closedCaptionsStyles.newMessagesButtonContainer as ViewStyle }>
<Button
accessibilityLabel = 'chat.newMessages'
// eslint-disable-next-line react/jsx-no-bind
icon = { () => (
<Icon
color = { BaseTheme.palette.icon04 }
size = { 20 }
src = { IconArrowDown } />
) }
labelKey = 'chat.newMessages'
onClick = { handleNewMessagesClick }
type = { BUTTON_TYPES.SECONDARY } />
</View>
)
}
</View>
);
}

View File

@@ -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.
*
@@ -44,7 +38,8 @@ export default {
},
emptyComponentText: {
color: BaseTheme.palette.text03,
...BaseTheme.typography.bodyLongBold,
color: BaseTheme.palette.text02,
textAlign: 'center'
},
@@ -113,13 +108,22 @@ export default {
},
emptyComponentWrapper: {
alignSelf: 'center',
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: BoxModel.padding,
paddingTop: '8%',
maxWidth: '80%'
},
emptyListStyle: {
flex: 1
},
emptyListContentContainer: {
alignItems: 'center',
flex: 1,
justifyContent: 'center'
},
disabledSendWrapper: {
alignSelf: 'center',
flex: 0,
@@ -133,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: {
@@ -205,11 +209,6 @@ export default {
fontSize: 13
},
chatContainer: {
backgroundColor: BaseTheme.palette.ui01,
flex: 1
},
tabContainer: {
flexDirection: 'row',
justifyContent: 'center'
@@ -270,3 +269,128 @@ export default {
flex: 1
}
};
/**
* Styles for the ClosedCaptions component.
*/
export const closedCaptionsStyles = {
container: {
backgroundColor: BaseTheme.palette.ui01,
flex: 1
},
emptyContentContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
emptyContent: {
alignItems: 'center',
justifyContent: 'center',
flex: 1,
padding: BaseTheme.spacing[3]
},
emptyStateText: {
...BaseTheme.typography.bodyLongBold,
color: BaseTheme.palette.text02,
textAlign: 'center',
maxWidth: '80%'
},
transcribingContainer: {
flex: 1
},
languageButtonContainer: {
justifyContent: 'space-between',
flexDirection: 'row',
padding: BaseTheme.spacing[3]
},
languageButtonText: {
...BaseTheme.typography.bodyShortRegularLarge,
color: BaseTheme.palette.text01,
marginHorizontal: BaseTheme.spacing[2]
},
languageButtonContent: {
flexDirection: 'row'
},
subtitleMessageContainer: {
backgroundColor: BaseTheme.palette.ui02,
borderRadius: BaseTheme.shape.borderRadius,
padding: BaseTheme.spacing[2],
maxWidth: '100%',
marginTop: BaseTheme.spacing[1]
},
subtitleMessageContent: {
maxWidth: '100%',
flex: 1
},
subtitleMessageHeader: {
...BaseTheme.typography.labelBold,
color: BaseTheme.palette.text02,
marginBottom: BaseTheme.spacing[1],
maxWidth: 130
},
subtitleMessageText: {
...BaseTheme.typography.bodyShortRegular,
color: BaseTheme.palette.text01
},
subtitleMessageTimestamp: {
...BaseTheme.typography.labelRegular,
color: BaseTheme.palette.text03,
marginTop: BaseTheme.spacing[1]
},
subtitleMessageInterim: {
opacity: 0.7
},
subtitlesGroupContainer: {
flexDirection: 'row',
marginBottom: BaseTheme.spacing[3]
},
subtitlesGroupAvatar: {
marginBottom: BaseTheme.spacing[10],
marginRight: BaseTheme.spacing[2],
alignSelf: 'flex-start',
width: 32
},
subtitlesGroupMessagesContainer: {
flexDirection: 'column',
flex: 1,
maxWidth: '100%'
},
subtitlesMessagesContainer: {
flex: 1,
position: 'relative',
height: '100%'
},
subtitlesMessagesList: {
padding: BaseTheme.spacing[4]
},
newMessagesButtonContainer: {
position: 'absolute',
bottom: BaseTheme.spacing[3],
alignSelf: 'center'
},
messagesContainer: {
display: 'flex',
flex: 1,
overflow: 'hidden'
}
};

View File

@@ -1,20 +1,13 @@
import React, { useCallback, useMemo, useState } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import { openDialog } from '../../../base/dialog/actions';
import Icon from '../../../base/icons/components/Icon';
import { IconSubtitles } from '../../../base/icons/svg';
import Button from '../../../base/ui/components/web/Button';
import { groupMessagesBySender } from '../../../base/util/messageGrouping';
import { StartRecordingDialog } from '../../../recording/components/Recording';
import { setRequestingSubtitles } from '../../../subtitles/actions.any';
import LanguageSelector from '../../../subtitles/components/web/LanguageSelector';
import { canStartSubtitles } from '../../../subtitles/functions.any';
import { ISubtitle } from '../../../subtitles/types';
import { isTranscribing } from '../../../transcribing/functions';
// @ts-ignore
import AbstractClosedCaptions, { AbstractProps } from '../AbstractClosedCaptions';
import { SubtitlesMessagesContainer } from './SubtitlesMessagesContainer';
@@ -79,72 +72,19 @@ const useStyles = makeStyles()(theme => {
*
* @returns {JSX.Element} - The ClosedCaptionsTab component.
*/
export default function ClosedCaptionsTab() {
const ClosedCaptionsTab = ({
canStartSubtitles,
filteredSubtitles,
groupedSubtitles,
isButtonPressed,
isTranscribing,
startClosedCaptions
}: AbstractProps): JSX.Element => {
const { classes, theme } = useStyles();
const dispatch = useDispatch();
const { t } = useTranslation();
const subtitles = useSelector((state: IReduxState) => state['features/subtitles'].subtitlesHistory);
const language = useSelector((state: IReduxState) => state['features/subtitles']._language);
const selectedLanguage = language?.replace('translation-languages:', '');
const _isTranscribing = useSelector(isTranscribing);
const _canStartSubtitles = useSelector(canStartSubtitles);
const [ isButtonPressed, setButtonPressed ] = useState(false);
const subtitlesError = useSelector((state: IReduxState) => state['features/subtitles']._hasError);
const isAsyncTranscriptionEnabled = useSelector((state: IReduxState) =>
state['features/base/conference'].conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription);
const filteredSubtitles = useMemo(() => {
// First, create a map of transcription messages by message ID
const transcriptionMessages = new Map(
subtitles
.filter(s => s.isTranscription)
.map(s => [ s.id, s ])
);
if (!selectedLanguage) {
// When no language is selected, show all original transcriptions
return Array.from(transcriptionMessages.values());
}
// Then, create a map of translation messages by message ID
const translationMessages = new Map(
subtitles
.filter(s => !s.isTranscription && s.language === selectedLanguage)
.map(s => [ s.id, s ])
);
// When a language is selected, for each transcription message:
// 1. Use its translation if available
// 2. Fall back to the original transcription if no translation exists
return Array.from(transcriptionMessages.values())
.filter((m: ISubtitle) => !m.interim)
.map(m => translationMessages.get(m.id) ?? m);
}, [ subtitles, selectedLanguage ]);
const groupedSubtitles = useMemo(() =>
groupMessagesBySender(filteredSubtitles), [ filteredSubtitles ]);
const startClosedCaptions = useCallback(() => {
if (isAsyncTranscriptionEnabled) {
dispatch(openDialog('StartRecordingDialog', StartRecordingDialog, {
recordAudioAndVideo: false
}));
} else {
if (isButtonPressed) {
return;
}
dispatch(setRequestingSubtitles(true, false, null));
setButtonPressed(true);
}
}, [ isAsyncTranscriptionEnabled, dispatch, isButtonPressed, openDialog, setButtonPressed ]);
if (subtitlesError && isButtonPressed && !isAsyncTranscriptionEnabled) {
setButtonPressed(false);
}
if (!_isTranscribing) {
if (_canStartSubtitles) {
if (!isTranscribing) {
if (canStartSubtitles) {
return (
<div className = { classes.emptyContent }>
<Button
@@ -159,10 +99,6 @@ export default function ClosedCaptionsTab() {
);
}
if (isButtonPressed && !isAsyncTranscriptionEnabled) {
setButtonPressed(false);
}
return (
<div className = { classes.emptyContent }>
<Icon
@@ -176,10 +112,6 @@ export default function ClosedCaptionsTab() {
);
}
if (isButtonPressed && !isAsyncTranscriptionEnabled) {
setButtonPressed(false);
}
return (
<div className = { classes.container }>
<LanguageSelector />
@@ -190,4 +122,6 @@ export default function ClosedCaptionsTab() {
</div>
</div>
);
}
};
export default AbstractClosedCaptions(ClosedCaptionsTab);

View File

@@ -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%'
},

View File

@@ -24,8 +24,6 @@ export default {
flexDirection: 'row',
height: 56,
justifyContent: 'center',
marginBottom: BaseTheme.spacing[0],
paddingBottom: BaseTheme.spacing[4],
width: '100%'
},

View File

@@ -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>
);
}

View File

@@ -588,7 +588,7 @@ function _registerForNativeEvents(store: IStore) {
}
if (transcription) {
store.dispatch(setRequestingSubtitles(true, false, null));
store.dispatch(setRequestingSubtitles(true, false, null, true));
}
});

View File

@@ -4,7 +4,7 @@ import { StyleProp, Text, TextStyle, View } from 'react-native';
import { navigationStyles } from './styles';
interface ITabBarLabelCounterProps {
activeUnreadNr: boolean;
activeUnreadNr?: boolean;
isFocused: boolean;
label: string;
unreadCount?: number;

View File

@@ -1,62 +0,0 @@
/* eslint-disable lines-around-comment */
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
getClientHeight,
getClientWidth
} from '../../../../../base/modal/components/functions';
import { setFocusedTab } from '../../../../../chat/actions.any';
import Chat from '../../../../../chat/components/native/Chat';
import { ChatTabs } from '../../../../../chat/constants';
import { getFocusedTab } from '../../../../../chat/functions';
import { resetUnreadPollsCount } from '../../../../../polls/actions';
import PollsPane from '../../../../../polls/components/native/PollsPane';
import { screen } from '../../../routes';
import { chatTabBarOptions } from '../../../screenOptions';
const ChatTab = createMaterialTopTabNavigator();
const ChatAndPolls = () => {
const clientHeight = useSelector(getClientHeight);
const clientWidth = useSelector(getClientWidth);
const dispatch = useDispatch();
const currentFocusedTab = useSelector(getFocusedTab);
const initialRouteName = currentFocusedTab === ChatTabs.POLLS
? screen.conference.chatandpolls.tab.polls
: screen.conference.chatandpolls.tab.chat;
return (
// @ts-ignore
<ChatTab.Navigator
backBehavior = 'none'
initialLayout = {{
height: clientHeight,
width: clientWidth
}}
initialRouteName = { initialRouteName }
screenOptions = { chatTabBarOptions }>
<ChatTab.Screen
component = { Chat }
listeners = {{
tabPress: () => {
dispatch(setFocusedTab(ChatTabs.CHAT));
}
}}
name = { screen.conference.chatandpolls.tab.chat } />
<ChatTab.Screen
component = { PollsPane }
listeners = {{
tabPress: () => {
dispatch(setFocusedTab(ChatTabs.POLLS));
dispatch(resetUnreadPollsCount);
}
}}
name = { screen.conference.chatandpolls.tab.polls } />
</ChatTab.Navigator>
);
};
export default ChatAndPolls;

View File

@@ -0,0 +1,88 @@
/* eslint-disable lines-around-comment */
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
getClientHeight,
getClientWidth
} from '../../../../../base/modal/components/functions.native';
import { setFocusedTab } from '../../../../../chat/actions.native';
import Chat from '../../../../../chat/components/native/Chat';
import ClosedCaptions from '../../../../../chat/components/native/ClosedCaptions';
import { ChatTabs } from '../../../../../chat/constants';
import { getFocusedTab, isChatDisabled } from '../../../../../chat/functions';
import { arePollsDisabled } from '../../../../../conference/functions.native';
import { resetUnreadPollsCount } from '../../../../../polls/actions';
import PollsPane from '../../../../../polls/components/native/PollsPane';
import { isCCTabEnabled } from '../../../../../subtitles/functions.any';
import { screen } from '../../../routes';
import { chatTabBarOptions } from '../../../screenOptions';
const ChatTab = createMaterialTopTabNavigator();
const ChatNavigator = () => {
const dispatch = useDispatch();
const clientHeight = useSelector(getClientHeight);
const clientWidth = useSelector(getClientWidth);
const currentFocusedTab = useSelector(getFocusedTab);
const isPollsTabDisabled = useSelector(arePollsDisabled);
const isChatTabDisabled = useSelector(isChatDisabled);
const isCCTabDisabled = !useSelector(isCCTabEnabled);
const initialRouteName
= currentFocusedTab === ChatTabs.POLLS ? screen.conference.chatTabs.tab.polls
: currentFocusedTab === ChatTabs.CLOSED_CAPTIONS ? screen.conference.chatTabs.tab.closedCaptions
: screen.conference.chatTabs.tab.chat;
return (
// @ts-ignore
<ChatTab.Navigator
backBehavior = 'none'
initialLayout = {{
height: clientHeight,
width: clientWidth
}}
initialRouteName = { initialRouteName }
screenOptions = { chatTabBarOptions }>
{
!isChatTabDisabled
&& <ChatTab.Screen
component = { Chat }
listeners = {{
tabPress: () => {
dispatch(setFocusedTab(ChatTabs.CHAT));
}
}}
name = { screen.conference.chatTabs.tab.chat } />
}
{
!isPollsTabDisabled
&& <ChatTab.Screen
component = { PollsPane }
listeners = {{
tabPress: () => {
dispatch(setFocusedTab(ChatTabs.POLLS));
dispatch(resetUnreadPollsCount);
}
}}
name = { screen.conference.chatTabs.tab.polls } />
}
{
!isCCTabDisabled
&& <ChatTab.Screen
component = { ClosedCaptions }
listeners = {{
tabPress: () => {
dispatch(setFocusedTab(ChatTabs.CLOSED_CAPTIONS));
}
}}
name = { screen.conference.chatTabs.tab.closedCaptions } />
}
</ChatTab.Navigator>
);
};
export default ChatNavigator;

View File

@@ -43,6 +43,7 @@ import SpeakerStats
import LanguageSelectorDialog
// @ts-ignore
from '../../../../../subtitles/components/native/LanguageSelectorDialog';
import { isCCTabEnabled } from '../../../../../subtitles/functions.any';
import Whiteboard from '../../../../../whiteboard/components/native/Whiteboard';
// @ts-ignore
import { screen } from '../../../routes';
@@ -68,7 +69,7 @@ import {
// @ts-ignore
} from '../../../screenOptions';
// @ts-ignore
import ChatAndPollsNavigator from '../../chat/components/ChatAndPollsNavigator';
import ChatNavigator from '../../chat/components/ChatNavigator';
// @ts-ignore
import LobbyNavigationContainer from '../../lobby/components/LobbyNavigationContainer';
// @ts-ignore
@@ -84,20 +85,32 @@ const ConferenceStack = createStackNavigator();
const ConferenceNavigationContainer = () => {
const isPollsDisabled = useSelector(arePollsDisabled);
const _isPollsEnabled = !isPollsDisabled;
const _isCCTabEnabled = useSelector(isCCTabEnabled);
const { t } = useTranslation();
let ChatScreen;
let chatScreenName;
let chatTitleString;
let chatTitle;
if (isPollsDisabled) {
ChatScreen = Chat;
chatScreenName = screen.conference.chat;
chatTitleString = 'chat.title';
} else {
ChatScreen = ChatAndPollsNavigator;
chatScreenName = screen.conference.chatandpolls.main;
chatTitleString = 'chat.titleWithPolls';
ChatScreen = ChatNavigator;
chatScreenName = screen.conference.chatTabs.main;
}
if (_isPollsEnabled || _isCCTabEnabled) {
const features = [
_isPollsEnabled ? t('chat.titleWithPolls') : '',
_isCCTabEnabled ? t('chat.titleWithCC') : ''
].filter(Boolean);
chatTitle = `${t('chat.titleWithFeatures')} ${features.join(' and ')}`;
} else {
chatTitle = t('chat.title');
}
const { t } = useTranslation();
return (
<NavigationContainer
@@ -117,7 +130,7 @@ const ConferenceNavigationContainer = () => {
name = { chatScreenName }
options = {{
...chatScreenOptions,
title: t(chatTitleString)
title: chatTitle
}} />
<ConferenceStack.Screen
component = { ParticipantsPane }

View File

@@ -16,6 +16,7 @@ import {
welcomeScreenOptions
} from '../../../screenOptions';
import {
goBack,
settingsNavigationContainerRef
} from '../SettingsNavigationContainerRef';
@@ -51,6 +52,7 @@ const SettingsNavigationContainer = ({ isInWelcomePage }: IProps) => {
const LanguageSelectScreen = useCallback(() =>
(<LanguageSelectView
goBack = { goBack }
isInWelcomePage = { isInWelcomePage } />)
, []);

View File

@@ -3,11 +3,12 @@ export const screen = {
breakoutRooms: 'Breakout Rooms',
carmode: 'Car Mode',
chat: 'Chat',
chatandpolls: {
main: 'Chat and Polls',
chatTabs: {
main: 'Chat',
tab: {
chat: 'Chat',
polls: 'Polls'
polls: 'Polls',
closedCaptions: 'CC'
}
},
container: 'Conference container',

View File

@@ -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>
);
};

View File

@@ -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 }

View File

@@ -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: {

View File

@@ -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>
);
};

View File

@@ -38,32 +38,36 @@ const PollsList = ({ setCreateMode }: IPollListProps) => {
scrollToBottom();
}, [ polls ]);
const renderEmptyComponent = useCallback(() => (
<View style = { pollsStyles.noPollContent as ViewStyle }>
<Icon
color = { BaseTheme.palette.icon03 }
size = { 100 }
src = { IconMessage } />
<Text
id = 'no-polls-text'
style = { pollsStyles.noPollText as TextStyle } >
{
t('polls.results.empty')
}
</Text>
</View>
), [ t ]);
const noPolls = listPolls.length === 0;
return (
<>
{
listPolls.length === 0
&& <View style = { pollsStyles.noPollContent as ViewStyle }>
<Icon
color = { BaseTheme.palette.icon03 }
size = { 160 }
src = { IconMessage } />
<Text
id = 'no-polls-text'
style = { pollsStyles.noPollText as TextStyle } >
{
t('polls.results.empty')
}
</Text>
</View>
}
<FlatList
data = { listPolls }
extraData = { listPolls }
// eslint-disable-next-line react/jsx-no-bind
keyExtractor = { (item, index) => index.toString() }
ref = { flatlistRef }
renderItem = { renderItem } />
</>
<FlatList
ListEmptyComponent = { renderEmptyComponent }
// @ts-ignore
contentContainerStyle = { noPolls && pollsStyles.emptyListContentContainer as ViewStyle }
data = { listPolls }
extraData = { listPolls }
// eslint-disable-next-line react/jsx-no-bind
keyExtractor = { (item, index) => index.toString() }
ref = { flatlistRef }
renderItem = { renderItem }
style = { noPolls && pollsStyles.emptyListStyle as ViewStyle } />
);
};

View File

@@ -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>
);

View File

@@ -117,21 +117,26 @@ export const resultsStyles = createStyleSheet({
export const pollsStyles = createStyleSheet({
emptyListStyle: {
flex: 1
},
emptyListContentContainer: {
alignItems: 'center',
flex: 1,
justifyContent: 'center'
},
noPollContent: {
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
top: '25%'
justifyContent: 'center'
},
noPollText: {
flex: 1,
color: BaseTheme.palette.text03,
...BaseTheme.typography.bodyLongBold,
color: BaseTheme.palette.text02,
textAlign: 'center',
maxWidth: '70%'
maxWidth: '80%'
},
pollItemContainer: {
@@ -153,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]
},
@@ -205,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: {

View File

@@ -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)
};
}

View File

@@ -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

View File

@@ -1,7 +1,7 @@
import { useNavigation } from '@react-navigation/native';
import React, { useCallback, useLayoutEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView, Text, TouchableHighlight, View, ViewStyle } from 'react-native';
import { GestureResponderEvent, ScrollView, Text, TouchableHighlight, View, ViewStyle } from 'react-native';
import { Edge } from 'react-native-safe-area-context';
import { useSelector } from 'react-redux';
@@ -11,12 +11,13 @@ import { IconArrowLeft } from '../../../base/icons/svg';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import BaseThemeNative from '../../../base/ui/components/BaseTheme.native';
import HeaderNavigationButton from '../../../mobile/navigation/components/HeaderNavigationButton';
import { goBack, navigate } from '../../../mobile/navigation/components/settings/SettingsNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import styles from './styles';
const LanguageSelectView = ({ isInWelcomePage }: { isInWelcomePage?: boolean; }) => {
const LanguageSelectView = ({ goBack, isInWelcomePage }: {
goBack?: (e?: GestureResponderEvent | React.MouseEvent) => void;
isInWelcomePage?: boolean;
}) => {
const { t } = useTranslation();
const navigation = useNavigation();
const { conference } = useSelector((state: IReduxState) => state['features/base/conference']);
@@ -25,7 +26,7 @@ const LanguageSelectView = ({ isInWelcomePage }: { isInWelcomePage?: boolean; })
const setLanguage = useCallback(language => () => {
i18next.changeLanguage(language);
conference?.setTranscriptionLanguage(language);
navigate(screen.settings.main);
goBack?.();
}, [ conference, i18next ]);
const headerLeft = () => (

View File

@@ -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
};
}

View File

@@ -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.

View File

@@ -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
});

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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