mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-26 16:40:19 +00:00
Compare commits
1 Commits
8997
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d32907178 |
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f #v6.1.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 #v6.2.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
os: [macos-latest, ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f #v6.1.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 #v6.2.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f #v6.1.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 #v6.2.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
@@ -75,7 +75,7 @@ jobs:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f #v6.1.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 #v6.2.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
rm -rf /host/usr/share/swift
|
||||
df -h /
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f #v6.1.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 #v6.2.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f #v6.1.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 #v6.2.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f #v6.1.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 #v6.2.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
@@ -1407,7 +1407,7 @@ PODS:
|
||||
- Yoga
|
||||
- react-native-performance (5.1.2):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (5.6.1):
|
||||
- react-native-safe-area-context (5.5.2):
|
||||
- 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: 2243039f43d10cb1ea30ec5ac57fc6d1448413f4
|
||||
react-native-safe-area-context: 0f7bf11598f9a61b7ceac8dc3f59ef98697e99e1
|
||||
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, true));
|
||||
APP.store.dispatch(setRequestingSubtitles(true, false, null));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@@ -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.6.1",
|
||||
"react-native-safe-area-context": "5.5.2",
|
||||
"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.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==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
@@ -42378,9 +42378,9 @@
|
||||
"integrity": "sha512-l5JOJphNzox9a9icL3T6O/gEqZuqWqcbejW04WPa10m0UanBdIYrNkPFl48B3ivWw3MabpjB6GiDYv7old9/fw=="
|
||||
},
|
||||
"react-native-safe-area-context": {
|
||||
"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=="
|
||||
"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=="
|
||||
},
|
||||
"react-native-screens": {
|
||||
"version": "4.11.1",
|
||||
|
||||
@@ -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.6.1",
|
||||
"react-native-safe-area-context": "5.5.2",
|
||||
"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",
|
||||
|
||||
@@ -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 = [ 'bottom', 'left', 'right' ],
|
||||
safeAreaInsets = [ 'left', 'right' ],
|
||||
style
|
||||
}: IProps) => {
|
||||
const renderContent = () => (
|
||||
@@ -78,8 +78,8 @@ const JitsiScreen = ({
|
||||
edges = { safeAreaInsets }
|
||||
style = { styles.safeArea }>
|
||||
{ children }
|
||||
{ footerComponent?.() }
|
||||
</SafeAreaView>
|
||||
{ footerComponent?.() }
|
||||
</JitsiKeyboardAvoidingView>
|
||||
);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export function openChat(participant?: IParticipant | undefined | Object, disabl
|
||||
if (disablePolls) {
|
||||
navigate(screen.conference.chat);
|
||||
} else {
|
||||
navigate(screen.conference.chatTabs.main);
|
||||
navigate(screen.conference.chatandpolls.main);
|
||||
}
|
||||
|
||||
dispatch(setFocusedTab(ChatTabs.CHAT));
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
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;
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
import { Route, useIsFocused } from '@react-navigation/native';
|
||||
import React, { Component, useEffect } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { connect } 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 { pollsStyles } from '../../../polls/components/native/styles';
|
||||
import { getUnreadPollCount } from '../../../polls/functions';
|
||||
import { closeChat, sendMessage } from '../../actions.native';
|
||||
import { ChatTabs } from '../../constants';
|
||||
import { getUnreadFilesCount } from '../../functions';
|
||||
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).
|
||||
*/
|
||||
@@ -52,7 +62,6 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,10 +76,14 @@ class Chat extends Component<IProps> {
|
||||
return (
|
||||
<JitsiScreen
|
||||
disableForcedKeyboardDismiss = { true }
|
||||
footerComponent = { this._renderFooter }
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
footerComponent = { () =>
|
||||
<ChatInputBar onSend = { this._onSendMessage } />
|
||||
}
|
||||
hasBottomTextInput = { true }
|
||||
hasExtraHeaderHeight = { true }
|
||||
style = { pollsStyles.pollPaneContainer as StyleType }>
|
||||
style = { styles.chatContainer }>
|
||||
{/* @ts-ignore */}
|
||||
<MessageContainer messages = { _messages } />
|
||||
<MessageRecipient privateMessageRecipient = { privateMessageRecipient } />
|
||||
@@ -78,16 +91,6 @@ class Chat extends Component<IProps> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the footer component.
|
||||
*
|
||||
* @private
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderFooter() {
|
||||
return <ChatInputBar onSend = { this._onSendMessage } />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a text message.
|
||||
*
|
||||
@@ -110,7 +113,9 @@ class Chat extends Component<IProps> {
|
||||
* @private
|
||||
* @returns {{
|
||||
* _messages: Array<Object>,
|
||||
* _unreadMessagesCount: number
|
||||
* _unreadMessagesCount: number,
|
||||
* _unreadPollsCount: number,
|
||||
* _unreadFilesCount: number
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
@@ -118,34 +123,34 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
|
||||
return {
|
||||
_messages: messages,
|
||||
_unreadMessagesCount: unreadMessagesCount
|
||||
_unreadMessagesCount: unreadMessagesCount,
|
||||
_unreadPollsCount: getUnreadPollCount(state),
|
||||
_unreadFilesCount: getUnreadFilesCount(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)((props: IProps) => {
|
||||
const { _unreadMessagesCount, dispatch, navigation, t } = props;
|
||||
|
||||
const isChatTabFocused = useSelector((state: IReduxState) => state['features/chat'].focusedTab === ChatTabs.CHAT);
|
||||
const { _unreadMessagesCount, _unreadPollsCount, _unreadFilesCount, dispatch, navigation, t } = props;
|
||||
const totalUnread = _unreadMessagesCount + _unreadPollsCount + _unreadFilesCount;
|
||||
const unreadMessagesNr = totalUnread > 0;
|
||||
|
||||
const isFocused = useIsFocused();
|
||||
|
||||
const activeUnreadMessagesNr = !isChatTabFocused && _unreadMessagesCount > 0;
|
||||
|
||||
useEffect(() => {
|
||||
navigation?.setOptions({
|
||||
tabBarLabel: () => (
|
||||
<TabBarLabelCounter
|
||||
activeUnreadNr = { activeUnreadMessagesNr }
|
||||
activeUnreadNr = { unreadMessagesNr }
|
||||
isFocused = { isFocused }
|
||||
label = { t('chat.tabs.chat') }
|
||||
unreadCount = { _unreadMessagesCount } />
|
||||
unreadCount = { totalUnread } />
|
||||
)
|
||||
});
|
||||
|
||||
return () => {
|
||||
isFocused && dispatch(closeChat());
|
||||
};
|
||||
}, [ isFocused, _unreadMessagesCount ]);
|
||||
}, [ isFocused, _unreadMessagesCount, _unreadPollsCount, _unreadFilesCount ]);
|
||||
|
||||
return (
|
||||
<Chat { ...props } />
|
||||
|
||||
@@ -43,7 +43,7 @@ class ChatButton extends AbstractButton<IProps> {
|
||||
override _handleClick() {
|
||||
this.props._isPollsDisabled
|
||||
? navigate(screen.conference.chat)
|
||||
: navigate(screen.conference.chatTabs.main);
|
||||
: navigate(screen.conference.chatandpolls.main);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ 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';
|
||||
@@ -84,6 +85,14 @@ 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
|
||||
@@ -100,7 +109,7 @@ class ChatInputBar extends Component<IProps, IState> {
|
||||
<View
|
||||
id = 'chat-input'
|
||||
style = { [
|
||||
styles.inputBar,
|
||||
inputBarStyles,
|
||||
this.state.addPadding ? styles.extraBarPadding : null
|
||||
] as ViewStyle[] }>
|
||||
<Input
|
||||
@@ -120,7 +129,6 @@ 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>
|
||||
);
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
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 ]);
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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);
|
||||
@@ -44,23 +44,19 @@ class MessageContainer extends Component<IProps, any> {
|
||||
*/
|
||||
override render() {
|
||||
const data = this._getMessagesGroupedBySender();
|
||||
const noMessages = data.length === 0;
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
ListEmptyComponent = { this._renderListEmptyComponent }
|
||||
bounces = { false }
|
||||
|
||||
// @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 }
|
||||
style = { noMessages && styles.emptyListStyle } />
|
||||
renderItem = { this._renderMessageGroup } />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -67,8 +67,8 @@ class PrivateMessageButton extends AbstractButton<IProps, any> {
|
||||
? navigate(screen.conference.chat, {
|
||||
privateMessageRecipient: this.props._participant
|
||||
})
|
||||
: navigate(screen.conference.chatTabs.main, {
|
||||
screen: screen.conference.chatTabs.tab.chat,
|
||||
: navigate(screen.conference.chatandpolls.main, {
|
||||
screen: screen.conference.chatandpolls.tab.chat,
|
||||
params: {
|
||||
privateMessageRecipient: this.props._participant
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,12 @@ const recipientContainer = {
|
||||
padding: BaseTheme.spacing[2]
|
||||
};
|
||||
|
||||
const inputBar = {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
};
|
||||
|
||||
/**
|
||||
* The styles of the feature chat.
|
||||
*
|
||||
@@ -38,8 +44,7 @@ export default {
|
||||
},
|
||||
|
||||
emptyComponentText: {
|
||||
...BaseTheme.typography.bodyLongBold,
|
||||
color: BaseTheme.palette.text02,
|
||||
color: BaseTheme.palette.text03,
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
@@ -108,20 +113,11 @@ export default {
|
||||
},
|
||||
|
||||
emptyComponentWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: BoxModel.padding,
|
||||
maxWidth: '80%'
|
||||
},
|
||||
|
||||
emptyListStyle: {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
emptyListContentContainer: {
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
padding: BoxModel.padding,
|
||||
paddingTop: '8%',
|
||||
maxWidth: '80%'
|
||||
},
|
||||
|
||||
disabledSendWrapper: {
|
||||
@@ -137,23 +133,23 @@ export default {
|
||||
* A special padding to avoid issues on some devices (such as Android devices with custom suggestions bar).
|
||||
*/
|
||||
extraBarPadding: {
|
||||
paddingBottom: BaseTheme.spacing[8]
|
||||
paddingBottom: 30
|
||||
},
|
||||
|
||||
inputBar: {
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
width: '100%'
|
||||
inputBarNarrow: {
|
||||
...inputBar,
|
||||
height: 112,
|
||||
marginHorizontal: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
sendButton: {
|
||||
marginRight: BaseTheme.spacing[5],
|
||||
marginLeft: BaseTheme.spacing[2]
|
||||
inputBarWide: {
|
||||
...inputBar,
|
||||
height: 88,
|
||||
marginHorizontal: BaseTheme.spacing[9]
|
||||
},
|
||||
|
||||
customInputContainer: {
|
||||
marginLeft: BaseTheme.spacing[5],
|
||||
flex: 1
|
||||
width: '75%'
|
||||
},
|
||||
|
||||
messageBubble: {
|
||||
@@ -209,6 +205,11 @@ export default {
|
||||
fontSize: 13
|
||||
},
|
||||
|
||||
chatContainer: {
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
tabContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center'
|
||||
@@ -269,128 +270,3 @@ 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'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback, useMemo, useState } 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';
|
||||
// @ts-ignore
|
||||
import AbstractClosedCaptions, { AbstractProps } from '../AbstractClosedCaptions';
|
||||
import { canStartSubtitles } from '../../../subtitles/functions.any';
|
||||
import { ISubtitle } from '../../../subtitles/types';
|
||||
import { isTranscribing } from '../../../transcribing/functions';
|
||||
|
||||
import { SubtitlesMessagesContainer } from './SubtitlesMessagesContainer';
|
||||
|
||||
@@ -72,19 +79,72 @@ const useStyles = makeStyles()(theme => {
|
||||
*
|
||||
* @returns {JSX.Element} - The ClosedCaptionsTab component.
|
||||
*/
|
||||
const ClosedCaptionsTab = ({
|
||||
canStartSubtitles,
|
||||
filteredSubtitles,
|
||||
groupedSubtitles,
|
||||
isButtonPressed,
|
||||
isTranscribing,
|
||||
startClosedCaptions
|
||||
}: AbstractProps): JSX.Element => {
|
||||
export default function ClosedCaptionsTab() {
|
||||
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);
|
||||
|
||||
if (!isTranscribing) {
|
||||
if (canStartSubtitles) {
|
||||
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) {
|
||||
return (
|
||||
<div className = { classes.emptyContent }>
|
||||
<Button
|
||||
@@ -99,6 +159,10 @@ const ClosedCaptionsTab = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (isButtonPressed && !isAsyncTranscriptionEnabled) {
|
||||
setButtonPressed(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { classes.emptyContent }>
|
||||
<Icon
|
||||
@@ -112,6 +176,10 @@ const ClosedCaptionsTab = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (isButtonPressed && !isAsyncTranscriptionEnabled) {
|
||||
setButtonPressed(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<LanguageSelector />
|
||||
@@ -122,6 +190,4 @@ const ClosedCaptionsTab = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AbstractClosedCaptions(ClosedCaptionsTab);
|
||||
}
|
||||
|
||||
@@ -21,9 +21,10 @@ const titleBarSafeView = {
|
||||
export default {
|
||||
|
||||
bottomContainer: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
bottom: BaseTheme.spacing[8]
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -100,7 +101,7 @@ export default {
|
||||
},
|
||||
|
||||
soundDeviceButton: {
|
||||
marginVertical: BaseTheme.spacing[3],
|
||||
marginBottom: BaseTheme.spacing[3],
|
||||
width: 240
|
||||
},
|
||||
|
||||
@@ -161,6 +162,7 @@ export default {
|
||||
videoStoppedLabel: {
|
||||
...BaseTheme.typography.bodyShortRegularLarge,
|
||||
color: BaseTheme.palette.text01,
|
||||
marginBottom: BaseTheme.spacing[3],
|
||||
textAlign: 'center',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
@@ -24,6 +24,8 @@ export default {
|
||||
flexDirection: 'row',
|
||||
height: 56,
|
||||
justifyContent: 'center',
|
||||
marginBottom: BaseTheme.spacing[0],
|
||||
paddingBottom: BaseTheme.spacing[4],
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { WithTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
SafeAreaView,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle
|
||||
@@ -512,7 +513,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<IProps, IState> {
|
||||
*/
|
||||
_renderShareMeetingButton() {
|
||||
return (
|
||||
<View
|
||||
<SafeAreaView
|
||||
style = { [
|
||||
styles.bottomBar as ViewStyle,
|
||||
this.state.bottomPadding ? styles.extraBarPadding : null
|
||||
@@ -523,7 +524,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<IProps, IState> {
|
||||
src = { IconShare }
|
||||
style = { styles.shareIcon } />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -588,7 +588,7 @@ function _registerForNativeEvents(store: IStore) {
|
||||
}
|
||||
|
||||
if (transcription) {
|
||||
store.dispatch(setRequestingSubtitles(true, false, null, true));
|
||||
store.dispatch(setRequestingSubtitles(true, false, null));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/* 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;
|
||||
@@ -1,88 +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.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;
|
||||
@@ -43,7 +43,6 @@ 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';
|
||||
@@ -69,7 +68,7 @@ import {
|
||||
// @ts-ignore
|
||||
} from '../../../screenOptions';
|
||||
// @ts-ignore
|
||||
import ChatNavigator from '../../chat/components/ChatNavigator';
|
||||
import ChatAndPollsNavigator from '../../chat/components/ChatAndPollsNavigator';
|
||||
// @ts-ignore
|
||||
import LobbyNavigationContainer from '../../lobby/components/LobbyNavigationContainer';
|
||||
// @ts-ignore
|
||||
@@ -85,32 +84,20 @@ const ConferenceStack = createStackNavigator();
|
||||
|
||||
const ConferenceNavigationContainer = () => {
|
||||
const isPollsDisabled = useSelector(arePollsDisabled);
|
||||
const _isPollsEnabled = !isPollsDisabled;
|
||||
const _isCCTabEnabled = useSelector(isCCTabEnabled);
|
||||
const { t } = useTranslation();
|
||||
|
||||
let ChatScreen;
|
||||
let chatScreenName;
|
||||
let chatTitle;
|
||||
let chatTitleString;
|
||||
|
||||
if (isPollsDisabled) {
|
||||
ChatScreen = Chat;
|
||||
chatScreenName = screen.conference.chat;
|
||||
chatTitleString = 'chat.title';
|
||||
} else {
|
||||
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');
|
||||
ChatScreen = ChatAndPollsNavigator;
|
||||
chatScreenName = screen.conference.chatandpolls.main;
|
||||
chatTitleString = 'chat.titleWithPolls';
|
||||
}
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<NavigationContainer
|
||||
@@ -130,7 +117,7 @@ const ConferenceNavigationContainer = () => {
|
||||
name = { chatScreenName }
|
||||
options = {{
|
||||
...chatScreenOptions,
|
||||
title: chatTitle
|
||||
title: t(chatTitleString)
|
||||
}} />
|
||||
<ConferenceStack.Screen
|
||||
component = { ParticipantsPane }
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
welcomeScreenOptions
|
||||
} from '../../../screenOptions';
|
||||
import {
|
||||
goBack,
|
||||
settingsNavigationContainerRef
|
||||
} from '../SettingsNavigationContainerRef';
|
||||
|
||||
@@ -52,7 +51,6 @@ const SettingsNavigationContainer = ({ isInWelcomePage }: IProps) => {
|
||||
|
||||
const LanguageSelectScreen = useCallback(() =>
|
||||
(<LanguageSelectView
|
||||
goBack = { goBack }
|
||||
isInWelcomePage = { isInWelcomePage } />)
|
||||
, []);
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@ export const screen = {
|
||||
breakoutRooms: 'Breakout Rooms',
|
||||
carmode: 'Car Mode',
|
||||
chat: 'Chat',
|
||||
chatTabs: {
|
||||
main: 'Chat',
|
||||
chatandpolls: {
|
||||
main: 'Chat and Polls',
|
||||
tab: {
|
||||
chat: 'Chat',
|
||||
polls: 'Polls',
|
||||
closedCaptions: 'CC'
|
||||
polls: 'Polls'
|
||||
}
|
||||
},
|
||||
container: 'Conference container',
|
||||
|
||||
@@ -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,6 +94,8 @@ function ParticipantItem({
|
||||
}: IProps) {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const participantNameContainerStyles
|
||||
= isKnockingParticipant ? styles.lobbyParticipantNameContainer : styles.participantNameContainer;
|
||||
|
||||
return (
|
||||
<View style = { styles.participantContainer as StyleProp<ViewStyle> } >
|
||||
@@ -104,8 +106,12 @@ function ParticipantItem({
|
||||
displayName = { displayName }
|
||||
participantId = { participantID }
|
||||
size = { 32 } />
|
||||
<View>
|
||||
<View style = { styles.participantNameContainer as StyleProp<ViewStyle> }>
|
||||
<View
|
||||
style = { [
|
||||
styles.participantDetailsContainer,
|
||||
raisedHand && styles.participantDetailsContainerRaisedHand
|
||||
] as StyleProp<ViewStyle> }>
|
||||
<View style = { participantNameContainerStyles as StyleProp<ViewStyle> }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.participantName as StyleProp<TextStyle> }>
|
||||
@@ -122,11 +128,13 @@ function ParticipantItem({
|
||||
</View>
|
||||
{
|
||||
!isKnockingParticipant
|
||||
&& <View style = { styles.participantStatesContainer as StyleProp<ViewStyle> }>
|
||||
<View>{ raisedHand && <RaisedHandIndicator /> }</View>
|
||||
<View style = { styles.participantStateVideo }>{ VideoStateIcons[videoMediaState] }</View>
|
||||
<View>{ AudioStateIcons[audioMediaState] }</View>
|
||||
</View>
|
||||
&& <>
|
||||
{ raisedHand && <RaisedHandIndicator /> }
|
||||
<View style = { styles.participantStatesContainer as StyleProp<ViewStyle> }>
|
||||
<View style = { styles.participantStateVideo }>{ VideoStateIcons[videoMediaState] }</View>
|
||||
<View>{ AudioStateIcons[audioMediaState] }</View>
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
</TouchableOpacity>
|
||||
{ !local && children }
|
||||
|
||||
@@ -6,8 +6,12 @@ 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],
|
||||
paddingVertical: BaseTheme.spacing[2]
|
||||
marginVertical: BaseTheme.spacing[2],
|
||||
position: 'relative',
|
||||
width: '70%'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -48,6 +52,13 @@ 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}.
|
||||
*/
|
||||
@@ -80,7 +91,8 @@ export default {
|
||||
flexDirection: 'row',
|
||||
height: BaseTheme.spacing[9],
|
||||
paddingLeft: BaseTheme.spacing[3],
|
||||
paddingRight: BaseTheme.spacing[3]
|
||||
paddingRight: BaseTheme.spacing[3],
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
participantContent: {
|
||||
@@ -94,11 +106,24 @@ export default {
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
participantNameContainer: {
|
||||
participantDetailsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
paddingLeft: BaseTheme.spacing[3],
|
||||
flexDirection: 'column',
|
||||
width: '73%'
|
||||
},
|
||||
|
||||
participantDetailsContainerRaisedHand: {
|
||||
width: '65%'
|
||||
},
|
||||
|
||||
participantNameContainer: {
|
||||
...participantNameContainer,
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
lobbyParticipantNameContainer: {
|
||||
...participantNameContainer,
|
||||
width: '40%'
|
||||
},
|
||||
|
||||
participantName: {
|
||||
@@ -114,19 +139,19 @@ export default {
|
||||
},
|
||||
|
||||
participantStatesContainer: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
marginLeft: 'auto'
|
||||
marginLeft: 'auto',
|
||||
width: '15%'
|
||||
},
|
||||
|
||||
participantStateVideo: {
|
||||
marginRight: BaseTheme.spacing[2]
|
||||
paddingRight: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
raisedHandIndicator: {
|
||||
backgroundColor: BaseTheme.palette.warning02,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
borderRadius: BaseTheme.shape.borderRadius / 2,
|
||||
height: BaseTheme.spacing[4],
|
||||
width: BaseTheme.spacing[4],
|
||||
marginLeft: 'auto',
|
||||
@@ -141,7 +166,7 @@ export default {
|
||||
|
||||
buttonAdmit: {
|
||||
position: 'absolute',
|
||||
right: BaseTheme.spacing[3]
|
||||
right: 16
|
||||
},
|
||||
|
||||
buttonReject: {
|
||||
@@ -176,14 +201,20 @@ export default {
|
||||
},
|
||||
|
||||
participantsPaneFooterContainer: {
|
||||
alignSelf: 'stretch',
|
||||
marginHorizontal: BaseTheme.spacing[5]
|
||||
alignItems: 'center',
|
||||
bottom: 0,
|
||||
height: 128,
|
||||
left: 0,
|
||||
paddingHorizontal: BaseTheme.spacing[4],
|
||||
right: 0
|
||||
},
|
||||
|
||||
participantsPaneFooter: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
marginLeft: 'auto'
|
||||
justifyContent: 'flex-end',
|
||||
paddingBottom: BaseTheme.spacing[3],
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
inviteButton: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { FlatList, SafeAreaView, TextInput, View, ViewStyle } from 'react-native';
|
||||
import { FlatList, Platform, TextInput, View, ViewStyle } from 'react-native';
|
||||
import { Divider } from 'react-native-paper';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
@@ -97,6 +97,9 @@ 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; }) => {
|
||||
|
||||
@@ -157,7 +160,7 @@ const PollCreate = (props: AbstractProps) => {
|
||||
), [ question ]);
|
||||
|
||||
return (
|
||||
<SafeAreaView style = { pollsStyles.pollCreateContainer as ViewStyle }>
|
||||
<View style = { pollsStyles.pollCreateContainer as ViewStyle }>
|
||||
<View style = { pollsStyles.pollCreateSubContainer as ViewStyle }>
|
||||
<FlatList
|
||||
ListHeaderComponent = { renderListHeaderComponent }
|
||||
@@ -166,7 +169,7 @@ const PollCreate = (props: AbstractProps) => {
|
||||
keyExtractor = { (item, index) => index.toString() }
|
||||
ref = { answerListRef }
|
||||
renderItem = { renderListItem } />
|
||||
<View style = { pollsStyles.pollCreateButtonsContainer as ViewStyle }>
|
||||
<View style = { pollCreateButtonsContainerStyles as ViewStyle }>
|
||||
<Button
|
||||
accessibilityLabel = 'polls.create.addOption'
|
||||
disabled = { answers.length >= ANSWERS_LIMIT }
|
||||
@@ -204,7 +207,7 @@ const PollCreate = (props: AbstractProps) => {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -38,36 +38,32 @@ 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 (
|
||||
<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 } />
|
||||
<>
|
||||
{
|
||||
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 } />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
@@ -41,27 +42,28 @@ const PollsPane = (props: AbstractProps) => {
|
||||
|
||||
}, [ isPollsTabFocused, unreadPollsCount ]);
|
||||
|
||||
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 ]);
|
||||
const createPollButtonStyles = Platform.OS === 'android'
|
||||
? pollsStyles.createPollButtonAndroid : pollsStyles.createPollButtonIos;
|
||||
|
||||
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 } />
|
||||
: <>
|
||||
<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 } />}
|
||||
</>
|
||||
}
|
||||
</JitsiScreen>
|
||||
);
|
||||
|
||||
@@ -117,26 +117,21 @@ export const resultsStyles = createStyleSheet({
|
||||
|
||||
export const pollsStyles = createStyleSheet({
|
||||
|
||||
emptyListStyle: {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
emptyListContentContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
noPollContent: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: '25%'
|
||||
},
|
||||
|
||||
noPollText: {
|
||||
...BaseTheme.typography.bodyLongBold,
|
||||
color: BaseTheme.palette.text02,
|
||||
flex: 1,
|
||||
color: BaseTheme.palette.text03,
|
||||
textAlign: 'center',
|
||||
maxWidth: '80%'
|
||||
maxWidth: '70%'
|
||||
},
|
||||
|
||||
pollItemContainer: {
|
||||
@@ -158,7 +153,13 @@ export const pollsStyles = createStyleSheet({
|
||||
marginTop: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
pollCreateButtonsContainer: {
|
||||
pollCreateButtonsContainerAndroid: {
|
||||
marginBottom: BaseTheme.spacing[8],
|
||||
marginHorizontal: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
pollCreateButtonsContainerIos: {
|
||||
marginBottom: BaseTheme.spacing[5],
|
||||
marginHorizontal: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
@@ -204,8 +205,14 @@ export const pollsStyles = createStyleSheet({
|
||||
color: BaseTheme.palette.action01
|
||||
},
|
||||
|
||||
createPollButton: {
|
||||
marginHorizontal: BaseTheme.spacing[5],
|
||||
createPollButtonIos: {
|
||||
marginHorizontal: 20,
|
||||
marginVertical: BaseTheme.spacing[5]
|
||||
},
|
||||
|
||||
createPollButtonAndroid: {
|
||||
marginHorizontal: 20,
|
||||
marginVertical: BaseTheme.spacing[5]
|
||||
},
|
||||
|
||||
pollPane: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { ComponentType, PureComponent } from 'react';
|
||||
import { TouchableWithoutFeedback, View } from 'react-native';
|
||||
import { TouchableWithoutFeedback } from 'react-native';
|
||||
import { Edge, SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
@@ -8,6 +9,7 @@ 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';
|
||||
|
||||
@@ -36,6 +38,11 @@ interface IProps {
|
||||
*/
|
||||
_styles: StyleType;
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the Toolbox is visible.
|
||||
*/
|
||||
_toolboxVisible: boolean;
|
||||
|
||||
/**
|
||||
* The width of the screen.
|
||||
*/
|
||||
@@ -80,12 +87,13 @@ class ReactionMenuDialog extends PureComponent<IProps> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const { _height, _participantCount, _styles, _width } = this.props;
|
||||
const { _height, _participantCount, _styles, _toolboxVisible, _width } = this.props;
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress = { this._onCancel }>
|
||||
<View
|
||||
<SafeAreaView
|
||||
edges = { [ 'bottom', 'left', 'right', !_toolboxVisible && 'top' ].filter(Boolean) as Edge[] }
|
||||
style = { [
|
||||
_styles,
|
||||
{
|
||||
@@ -95,7 +103,7 @@ class ReactionMenuDialog extends PureComponent<IProps> {
|
||||
<ReactionMenu
|
||||
onCancel = { this._onCancel }
|
||||
overflowMenu = { false } />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
@@ -130,7 +138,8 @@ 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)
|
||||
_participantCount: getParticipantCount(state),
|
||||
_toolboxVisible: isToolboxVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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, true));
|
||||
dispatch(setRequestingSubtitles(true, _displaySubtitles, _subtitlesLanguage));
|
||||
} else {
|
||||
_conference?.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: this.state.shouldRecordTranscription
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import React, { useCallback, useLayoutEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GestureResponderEvent, ScrollView, Text, TouchableHighlight, View, ViewStyle } from 'react-native';
|
||||
import { ScrollView, Text, TouchableHighlight, View, ViewStyle } from 'react-native';
|
||||
import { Edge } from 'react-native-safe-area-context';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
@@ -11,13 +11,12 @@ 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 = ({ goBack, isInWelcomePage }: {
|
||||
goBack?: (e?: GestureResponderEvent | React.MouseEvent) => void;
|
||||
isInWelcomePage?: boolean;
|
||||
}) => {
|
||||
const LanguageSelectView = ({ isInWelcomePage }: { isInWelcomePage?: boolean; }) => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation();
|
||||
const { conference } = useSelector((state: IReduxState) => state['features/base/conference']);
|
||||
@@ -26,7 +25,7 @@ const LanguageSelectView = ({ goBack, isInWelcomePage }: {
|
||||
const setLanguage = useCallback(language => () => {
|
||||
i18next.changeLanguage(language);
|
||||
conference?.setTranscriptionLanguage(language);
|
||||
goBack?.();
|
||||
navigate(screen.settings.main);
|
||||
}, [ conference, i18next ]);
|
||||
|
||||
const headerLeft = () => (
|
||||
|
||||
@@ -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} forceBackendRecordingOn - Whether to force that backend recording is on.
|
||||
* @param {boolean} backendRecordingOn - Whether backend recording is on.
|
||||
* @returns {{
|
||||
* type: SET_REQUESTING_SUBTITLES,
|
||||
* backendRecordingOn: boolean,
|
||||
@@ -95,13 +95,11 @@ export function toggleRequestingSubtitles() {
|
||||
export function setRequestingSubtitles(
|
||||
enabled: boolean,
|
||||
displaySubtitles = true,
|
||||
language: string | null = `translation-languages:${DEFAULT_LANGUAGE}`,
|
||||
forceBackendRecordingOn: boolean = false) {
|
||||
language: string | null = `translation-languages:${DEFAULT_LANGUAGE}`) {
|
||||
return {
|
||||
type: SET_REQUESTING_SUBTITLES,
|
||||
displaySubtitles,
|
||||
enabled,
|
||||
forceBackendRecordingOn,
|
||||
language
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,13 +51,6 @@ 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, action.forceBackendRecordingOn);
|
||||
_requestingSubtitlesChange(store, action.enabled, action.language);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -344,16 +344,13 @@ 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,
|
||||
forceBackendRecordingOn: boolean = false) {
|
||||
language?: string | null) {
|
||||
const state = getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
const backendRecordingOn = conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription;
|
||||
@@ -378,9 +375,7 @@ function _requestingSubtitlesChange(
|
||||
}));
|
||||
dispatch(setSubtitlesError(true));
|
||||
});
|
||||
}
|
||||
|
||||
if (backendRecordingOn || forceBackendRecordingOn) {
|
||||
} else {
|
||||
conference?.getMetadataHandler()?.setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: true
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user