diff --git a/.circleci/config.yml b/.circleci/config.yml index 14ba8f40ba..bcb01f2a85 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ defaults: &defaults macos: &macos macos: - xcode: "11.2.1" + xcode: "11.5.0" bash-env: &bash-env BASH_ENV: "~/.nvm/nvm.sh" @@ -33,14 +33,12 @@ save-npm-cache-mac: &save-npm-cache-mac - ./node_modules install-node: &install-node - name: Install Node 10 + name: Install Node command: | - curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.6/install.sh | bash + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash source ~/.nvm/nvm.sh - # https://github.com/creationix/nvm/issues/1394 - set +e - nvm install 10 - echo 'export PATH="/home/circleci/.nvm/versions/node/v10.20.1/bin:$PATH"' >> ~/.bash_profile + INSTALLED_NODE=`nvm which current` + echo "export PATH=\"${INSTALLED_NODE%%/node}:\$PATH\"" >> ~/.bash_profile source ~/.bash_profile restore-gems-cache: &restore-gems-cache diff --git a/.gitignore b/.gitignore index 6e797d850a..fee7bf8651 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,5 @@ buck-out/ coverage .vscode/ +e2e/docker/rc_test_env/docker-compose.yml +e2e/docker/data/db \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 3182eff242..8bf5d0f933 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -139,7 +139,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode VERSIONCODE as Integer - versionName "4.8.0" + versionName "4.10.0" vectorDrawables.useSupportLibrary = true manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60" // See note below! diff --git a/app/constants/colors.js b/app/constants/colors.js index 7b9d567630..0b0a8ae7fb 100644 --- a/app/constants/colors.js +++ b/app/constants/colors.js @@ -1,5 +1,3 @@ -import { isIOS, isAndroid } from '../utils/deviceInfo'; - export const STATUS_COLORS = { online: '#2de0a5', busy: '#f5455c', @@ -8,7 +6,7 @@ export const STATUS_COLORS = { }; export const SWITCH_TRACK_COLOR = { - false: isAndroid ? '#f5455c' : null, + false: '#f5455c', true: '#2de0a5' }; @@ -34,11 +32,11 @@ export const themes = { separatorColor: '#cbcbcc', navbarBackground: '#ffffff', headerBorder: '#B2B2B2', - headerBackground: isIOS ? '#f8f8f8' : '#2f343d', + headerBackground: '#EEEFF1', headerSecondaryBackground: '#ffffff', - headerTintColor: isAndroid ? '#ffffff' : '#1d74f5', - headerTitleColor: isAndroid ? '#ffffff' : '#0d0e12', - headerSecondaryText: isAndroid ? '#9ca2a8' : '#1d74f5', + headerTintColor: '#6C727A', + headerTitleColor: '#0C0D0F', + headerSecondaryText: '#1d74f5', toastBackground: '#0C0D0F', videoBackground: '#1f2329', favoriteBackground: '#ffbb00', @@ -63,7 +61,7 @@ export const themes = { chatComponentBackground: '#192132', auxiliaryBackground: '#07101e', bannerBackground: '#0e1f38', - titleText: '#FFFFFF', + titleText: '#f9f9f9', bodyText: '#e8ebed', backdropColor: '#000000', dangerColor: '#f5455c', @@ -80,9 +78,9 @@ export const themes = { headerBorder: '#2F3A4B', headerBackground: '#0b182c', headerSecondaryBackground: '#0b182c', - headerTintColor: isAndroid ? '#ffffff' : '#1d74f5', - headerTitleColor: '#FFFFFF', - headerSecondaryText: isAndroid ? '#9297a2' : '#1d74f5', + headerTintColor: '#f9f9f9', + headerTitleColor: '#f9f9f9', + headerSecondaryText: '#9297a2', toastBackground: '#0C0D0F', videoBackground: '#1f2329', favoriteBackground: '#ffbb00', @@ -124,9 +122,9 @@ export const themes = { headerBorder: '#323232', headerBackground: '#0d0d0d', headerSecondaryBackground: '#0d0d0d', - headerTintColor: isAndroid ? '#ffffff' : '#1e9bfe', + headerTintColor: '#f9f9f9', headerTitleColor: '#f9f9f9', - headerSecondaryText: isAndroid ? '#b2b8c6' : '#1e9bfe', + headerSecondaryText: '#b2b8c6', toastBackground: '#0C0D0F', videoBackground: '#1f2329', favoriteBackground: '#ffbb00', diff --git a/app/containers/ActionSheet/styles.js b/app/containers/ActionSheet/styles.js index d87c35f126..76078233b5 100644 --- a/app/containers/ActionSheet/styles.js +++ b/app/containers/ActionSheet/styles.js @@ -29,7 +29,8 @@ export default StyleSheet.create({ }, handle: { justifyContent: 'center', - alignItems: 'center' + alignItems: 'center', + paddingBottom: 8 }, handleIndicator: { width: 40, diff --git a/app/containers/DisclosureIndicator.js b/app/containers/DisclosureIndicator.js index 9d574de6c8..e33dbe8168 100644 --- a/app/containers/DisclosureIndicator.js +++ b/app/containers/DisclosureIndicator.js @@ -17,7 +17,7 @@ const styles = StyleSheet.create({ export const DisclosureImage = React.memo(({ theme }) => ( )); diff --git a/app/containers/Header/index.js b/app/containers/Header/index.js index 249b832e46..2137a71ad5 100644 --- a/app/containers/Header/index.js +++ b/app/containers/Header/index.js @@ -20,6 +20,11 @@ export const getHeaderHeight = (isLandscape) => { return 56; }; +export const getHeaderTitlePosition = insets => ({ + left: 60 + insets.left, + right: 80 + insets.right +}); + const styles = StyleSheet.create({ container: { height: headerHeight, diff --git a/app/containers/HeaderButton.js b/app/containers/HeaderButton.js index 3ac44d4534..e712a7e1eb 100644 --- a/app/containers/HeaderButton.js +++ b/app/containers/HeaderButton.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { HeaderButtons, HeaderButton, Item } from 'react-navigation-header-buttons'; import { CustomIcon } from '../lib/Icons'; -import { isIOS, isAndroid } from '../utils/deviceInfo'; +import { isIOS } from '../utils/deviceInfo'; import { themes } from '../constants/colors'; import I18n from '../i18n'; import { withTheme } from '../theme'; @@ -15,11 +15,7 @@ const CustomHeaderButton = React.memo(withTheme(({ theme, ...props }) => ( {...props} IconComponent={CustomIcon} iconSize={headerIconSize} - color={ - isAndroid - ? themes[theme].headerTitleColor - : themes[theme].headerTintColor - } + color={themes[theme].headerTintColor} /> ))); @@ -32,7 +28,7 @@ export const CustomHeaderButtons = React.memo(props => ( export const DrawerButton = React.memo(({ navigation, testID, ...otherProps }) => ( - + )); diff --git a/app/containers/LoginServices.js b/app/containers/LoginServices.js index b569290330..11ffc6d97d 100644 --- a/app/containers/LoginServices.js +++ b/app/containers/LoginServices.js @@ -5,29 +5,32 @@ import { import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Base64 } from 'js-base64'; +import * as AppleAuthentication from 'expo-apple-authentication'; import { withTheme } from '../theme'; import sharedStyles from '../views/Styles'; import { themes } from '../constants/colors'; -import { loginRequest as loginRequestAction } from '../actions/login'; import Button from './Button'; import OrSeparator from './OrSeparator'; import Touch from '../utils/touch'; import I18n from '../i18n'; import random from '../utils/random'; +import RocketChat from '../lib/rocketchat'; +const BUTTON_HEIGHT = 48; const SERVICE_HEIGHT = 58; +const BORDER_RADIUS = 2; const SERVICES_COLLAPSED_HEIGHT = 174; const styles = StyleSheet.create({ serviceButton: { - borderRadius: 2, + borderRadius: BORDER_RADIUS, marginBottom: 10 }, serviceButtonContainer: { - borderRadius: 2, + borderRadius: BORDER_RADIUS, width: '100%', - height: 48, + height: BUTTON_HEIGHT, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', @@ -187,6 +190,21 @@ class LoginServices extends React.PureComponent { this.openOAuth({ url, ssoToken, authType: 'cas' }); } + onPressAppleLogin = async() => { + try { + const { fullName, email, identityToken } = await AppleAuthentication.signInAsync({ + requestedScopes: [ + AppleAuthentication.AppleAuthenticationScope.FULL_NAME, + AppleAuthentication.AppleAuthenticationScope.EMAIL + ] + }); + + await RocketChat.loginOAuthOrSso({ fullName, email, identityToken }); + } catch { + // Do nothing + } + } + getOAuthState = () => { const credentialToken = random(43); return Base64.encodeURI(JSON.stringify({ loginStyle: 'popup', credentialToken, isCordova: true })); @@ -262,6 +280,7 @@ class LoginServices extends React.PureComponent { } renderItem = (service) => { + const { CAS_enabled, theme } = this.props; let { name } = service; name = name === 'meteor-developer' ? 'meteor' : name; const icon = `icon_${ name }`; @@ -285,11 +304,27 @@ class LoginServices extends React.PureComponent { onPress = () => this.onPressCas(); break; } + case 'apple': { + onPress = () => this.onPressAppleLogin(); + break; + } default: break; } + + if (name === 'apple') { + return ( + + ); + } + name = name.charAt(0).toUpperCase() + name.slice(1); - const { CAS_enabled, theme } = this.props; let buttonText; if (isSaml || (service.service === 'cas' && CAS_enabled)) { buttonText = {name}; @@ -356,8 +391,4 @@ const mapStateToProps = state => ({ services: state.login.services }); -const mapDispatchToProps = dispatch => ({ - loginRequest: params => dispatch(loginRequestAction(params)) -}); - -export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LoginServices)); +export default connect(mapStateToProps)(withTheme(LoginServices)); diff --git a/app/containers/MessageActions/Header.js b/app/containers/MessageActions/Header.js index 5db34acec9..7847069e18 100644 --- a/app/containers/MessageActions/Header.js +++ b/app/containers/MessageActions/Header.js @@ -14,17 +14,20 @@ import { Button } from '../ActionSheet'; import { useDimensions } from '../../dimensions'; export const HEADER_HEIGHT = 36; +const ITEM_SIZE = 36; +const CONTAINER_MARGIN = 8; +const ITEM_MARGIN = 8; const styles = StyleSheet.create({ container: { alignItems: 'center', - marginHorizontal: 8 + marginHorizontal: CONTAINER_MARGIN }, headerItem: { - height: 36, - width: 36, - borderRadius: 20, - marginHorizontal: 8, + height: ITEM_SIZE, + width: ITEM_SIZE, + borderRadius: ITEM_SIZE / 2, + marginHorizontal: ITEM_MARGIN, justifyContent: 'center', alignItems: 'center' }, @@ -84,7 +87,7 @@ HeaderFooter.propTypes = { }; const Header = React.memo(({ - handleReaction, server, message, theme + handleReaction, server, message, isMasterDetail, theme }) => { const [items, setItems] = useState([]); const { width, height } = useDimensions(); @@ -96,8 +99,8 @@ const Header = React.memo(({ let freqEmojis = await freqEmojiCollection.query().fetch(); const isLandscape = width > height; - const size = isLandscape ? width / 2 : width; - const quantity = (size / 50) - 1; + const size = (isLandscape || isMasterDetail ? width / 2 : width) - (CONTAINER_MARGIN * 2); + const quantity = (size / (ITEM_SIZE + (ITEM_MARGIN * 2))) - 1; freqEmojis = freqEmojis.concat(DEFAULT_EMOJIS).slice(0, quantity); setItems(freqEmojis); @@ -135,6 +138,7 @@ Header.propTypes = { handleReaction: PropTypes.func, server: PropTypes.string, message: PropTypes.object, + isMasterDetail: PropTypes.bool, theme: PropTypes.string }; export default withTheme(Header); diff --git a/app/containers/MessageActions/index.js b/app/containers/MessageActions/index.js index 4ba286db98..7846e1d70d 100644 --- a/app/containers/MessageActions/index.js +++ b/app/containers/MessageActions/index.js @@ -32,7 +32,8 @@ const MessageActions = React.memo(forwardRef(({ Message_AllowEditing_BlockEditInMinutes, Message_AllowPinning, Message_AllowStarring, - Message_Read_Receipt_Store_Users + Message_Read_Receipt_Store_Users, + isMasterDetail }, ref) => { let permissions = {}; const { showActionSheet, hideActionSheet } = useActionSheet(); @@ -116,7 +117,12 @@ const MessageActions = React.memo(forwardRef(({ const handleEdit = message => editInit(message); const handleCreateDiscussion = (message) => { - Navigation.navigate('CreateDiscussionView', { message, channel: room }); + const params = { message, channel: room, showCloseModal: true }; + if (isMasterDetail) { + Navigation.navigate('ModalStackNavigator', { screen: 'CreateDiscussionView', params }); + } else { + Navigation.navigate('NewMessageStackNavigator', { screen: 'CreateDiscussionView', params }); + } }; const handleUnread = async(message) => { @@ -377,6 +383,7 @@ const MessageActions = React.memo(forwardRef(({
) : null) @@ -412,7 +419,8 @@ const mapStateToProps = state => ({ Message_AllowEditing_BlockEditInMinutes: state.settings.Message_AllowEditing_BlockEditInMinutes, Message_AllowPinning: state.settings.Message_AllowPinning, Message_AllowStarring: state.settings.Message_AllowStarring, - Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users + Message_Read_Receipt_Store_Users: state.settings.Message_Read_Receipt_Store_Users, + isMasterDetail: state.app.isMasterDetail }); export default connect(mapStateToProps, null, null, { forwardRef: true })(MessageActions); diff --git a/app/containers/MessageBox/EmojiKeyboard.js b/app/containers/MessageBox/EmojiKeyboard.js index f8bc13019b..8d552509ca 100644 --- a/app/containers/MessageBox/EmojiKeyboard.js +++ b/app/containers/MessageBox/EmojiKeyboard.js @@ -17,7 +17,7 @@ export default class EmojiKeyboard extends React.PureComponent { constructor(props) { super(props); const state = store.getState(); - this.baseUrl = state.server.server; + this.baseUrl = state.share.server || state.server.server; } onEmojiSelected = (emoji) => { diff --git a/app/containers/MessageBox/RecordAudio.js b/app/containers/MessageBox/RecordAudio.js new file mode 100644 index 0000000000..b9e94988ca --- /dev/null +++ b/app/containers/MessageBox/RecordAudio.js @@ -0,0 +1,226 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { View, Text } from 'react-native'; +import { Audio } from 'expo-av'; +import { BorderlessButton } from 'react-native-gesture-handler'; +import { getInfoAsync } from 'expo-file-system'; +import { deactivateKeepAwake, activateKeepAwake } from 'expo-keep-awake'; + +import styles from './styles'; +import I18n from '../../i18n'; +import { themes } from '../../constants/colors'; +import { CustomIcon } from '../../lib/Icons'; + +const RECORDING_EXTENSION = '.aac'; +const RECORDING_SETTINGS = { + android: { + extension: RECORDING_EXTENSION, + outputFormat: Audio.RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_AAC_ADTS, + audioEncoder: Audio.RECORDING_OPTION_ANDROID_AUDIO_ENCODER_AAC, + sampleRate: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.android.sampleRate, + numberOfChannels: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.android.numberOfChannels, + bitRate: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.android.bitRate + }, + ios: { + extension: RECORDING_EXTENSION, + audioQuality: Audio.RECORDING_OPTION_IOS_AUDIO_QUALITY_MIN, + sampleRate: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.ios.sampleRate, + numberOfChannels: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.ios.numberOfChannels, + bitRate: Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY.ios.bitRate, + outputFormat: Audio.RECORDING_OPTION_IOS_OUTPUT_FORMAT_MPEG4AAC + } +}; +const RECORDING_MODE = { + allowsRecordingIOS: true, + playsInSilentModeIOS: true, + staysActiveInBackground: false, + shouldDuckAndroid: true, + playThroughEarpieceAndroid: false, + interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX, + interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX +}; + +const formatTime = function(seconds) { + let minutes = Math.floor(seconds / 60); + seconds %= 60; + if (minutes < 10) { minutes = `0${ minutes }`; } + if (seconds < 10) { seconds = `0${ seconds }`; } + return `${ minutes }:${ seconds }`; +}; + +export default class RecordAudio extends React.PureComponent { + static propTypes = { + theme: PropTypes.string, + recordingCallback: PropTypes.func, + onFinish: PropTypes.func + } + + constructor(props) { + super(props); + this.isRecorderBusy = false; + this.state = { + isRecording: false, + recordingDurationMillis: 0 + }; + } + + componentDidUpdate() { + const { recordingCallback } = this.props; + const { isRecording } = this.state; + + recordingCallback(isRecording); + } + + componentWillUnmount() { + if (this.recording) { + this.cancelRecordingAudio(); + } + } + + get duration() { + const { recordingDurationMillis } = this.state; + return formatTime(Math.floor(recordingDurationMillis / 1000)); + } + + isRecordingPermissionGranted = async() => { + try { + const permission = await Audio.getPermissionsAsync(); + if (permission.status === 'granted') { + return true; + } + await Audio.requestPermissionsAsync(); + } catch { + // Do nothing + } + return false; + } + + onRecordingStatusUpdate = (status) => { + this.setState({ + isRecording: status.isRecording, + recordingDurationMillis: status.durationMillis + }); + } + + startRecordingAudio = async() => { + if (!this.isRecorderBusy) { + this.isRecorderBusy = true; + try { + const canRecord = await this.isRecordingPermissionGranted(); + if (canRecord) { + await Audio.setAudioModeAsync(RECORDING_MODE); + + this.recording = new Audio.Recording(); + await this.recording.prepareToRecordAsync(RECORDING_SETTINGS); + this.recording.setOnRecordingStatusUpdate(this.onRecordingStatusUpdate); + + await this.recording.startAsync(); + activateKeepAwake(); + } else { + await Audio.requestPermissionsAsync(); + } + } catch (error) { + // Do nothing + } + this.isRecorderBusy = false; + } + }; + + finishRecordingAudio = async() => { + if (!this.isRecorderBusy) { + const { onFinish } = this.props; + + this.isRecorderBusy = true; + try { + await this.recording.stopAndUnloadAsync(); + + const fileURI = this.recording.getURI(); + const fileData = await getInfoAsync(fileURI); + const fileInfo = { + name: `${ Date.now() }.aac`, + mime: 'audio/aac', + type: 'audio/aac', + store: 'Uploads', + path: fileURI, + size: fileData.size + }; + + onFinish(fileInfo); + } catch (error) { + // Do nothing + } + this.setState({ isRecording: false, recordingDurationMillis: 0 }); + deactivateKeepAwake(); + this.isRecorderBusy = false; + } + }; + + cancelRecordingAudio = async() => { + if (!this.isRecorderBusy) { + this.isRecorderBusy = true; + try { + await this.recording.stopAndUnloadAsync(); + } catch (error) { + // Do nothing + } + this.setState({ isRecording: false, recordingDurationMillis: 0 }); + deactivateKeepAwake(); + this.isRecorderBusy = false; + } + }; + + render() { + const { theme } = this.props; + const { isRecording } = this.state; + + if (!isRecording) { + return ( + + + + ); + } + + return ( + + + + + + + {this.duration} + + + + + + + ); + } +} diff --git a/app/containers/MessageBox/Recording.js b/app/containers/MessageBox/Recording.js deleted file mode 100644 index d688de3f86..0000000000 --- a/app/containers/MessageBox/Recording.js +++ /dev/null @@ -1,172 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { - View, PermissionsAndroid, Text -} from 'react-native'; -import { AudioRecorder, AudioUtils } from 'react-native-audio'; -import { BorderlessButton } from 'react-native-gesture-handler'; -import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake'; -import * as FileSystem from 'expo-file-system'; - -import styles from './styles'; -import I18n from '../../i18n'; -import { isIOS, isAndroid } from '../../utils/deviceInfo'; -import { CustomIcon } from '../../lib/Icons'; -import { themes } from '../../constants/colors'; -import SafeAreaView from '../SafeAreaView'; - -export const _formatTime = function(seconds) { - let minutes = Math.floor(seconds / 60); - seconds %= 60; - if (minutes < 10) { minutes = `0${ minutes }`; } - if (seconds < 10) { seconds = `0${ seconds }`; } - return `${ minutes }:${ seconds }`; -}; - -export default class extends React.PureComponent { - static async permission() { - if (!isAndroid) { - return true; - } - - const rationale = { - title: I18n.t('Microphone_Permission'), - message: I18n.t('Microphone_Permission_Message') - }; - - const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, rationale); - return result === true || result === PermissionsAndroid.RESULTS.GRANTED; - } - - static propTypes = { - theme: PropTypes.string, - onFinish: PropTypes.func.isRequired - } - - constructor() { - super(); - - this.recordingCanceled = false; - this.recording = true; - this.name = `${ Date.now() }.aac`; - this.state = { - currentTime: '00:00' - }; - } - - componentDidMount() { - const audioPath = `${ AudioUtils.CachesDirectoryPath }/${ this.name }`; - - AudioRecorder.prepareRecordingAtPath(audioPath, { - SampleRate: 22050, - Channels: 1, - AudioQuality: 'Low', - AudioEncoding: 'aac', - OutputFormat: 'aac_adts' - }); - - AudioRecorder.onProgress = (data) => { - this.setState({ - currentTime: _formatTime(Math.floor(data.currentTime)) - }); - }; - // - AudioRecorder.onFinished = (data) => { - if (!this.recordingCanceled && isIOS) { - this.finishRecording(data.status === 'OK', data.audioFileURL, data.audioFileSize); - } - }; - AudioRecorder.startRecording(); - - activateKeepAwake(); - } - - componentWillUnmount() { - if (this.recording) { - this.cancelAudioMessage(); - } - - deactivateKeepAwake(); - } - - finishRecording = (didSucceed, filePath, size) => { - const { onFinish } = this.props; - if (!didSucceed) { - return onFinish && onFinish(didSucceed); - } - const fileInfo = { - name: this.name, - mime: 'audio/aac', - type: 'audio/aac', - store: 'Uploads', - path: filePath, - size - }; - return onFinish && onFinish(fileInfo); - } - - finishAudioMessage = async() => { - try { - this.recording = false; - let filePath = await AudioRecorder.stopRecording(); - if (isAndroid) { - filePath = filePath.startsWith('file://') ? filePath : `file://${ filePath }`; - const data = await FileSystem.getInfoAsync(decodeURIComponent(filePath), { size: true }); - this.finishRecording(true, filePath, data.size); - } - } catch (err) { - this.finishRecording(false); - } - } - - cancelAudioMessage = async() => { - this.recording = false; - this.recordingCanceled = true; - await AudioRecorder.stopRecording(); - return this.finishRecording(false); - } - - render() { - const { currentTime } = this.state; - const { theme } = this.props; - - return ( - - - - - - {currentTime} - - - - - - ); - } -} diff --git a/app/containers/MessageBox/RightButtons.android.js b/app/containers/MessageBox/RightButtons.android.js index 88d6e72884..6da835642e 100644 --- a/app/containers/MessageBox/RightButtons.android.js +++ b/app/containers/MessageBox/RightButtons.android.js @@ -2,23 +2,19 @@ import React from 'react'; import PropTypes from 'prop-types'; import { View } from 'react-native'; -import { SendButton, AudioButton, ActionsButton } from './buttons'; +import { SendButton, ActionsButton } from './buttons'; import styles from './styles'; const RightButtons = React.memo(({ - theme, showSend, submit, recordAudioMessage, recordAudioMessageEnabled, showMessageBoxActions, isActionsEnabled + theme, showSend, submit, showMessageBoxActions, isActionsEnabled }) => { if (showSend) { return ; } - if (recordAudioMessageEnabled || isActionsEnabled) { - return ( - <> - {recordAudioMessageEnabled ? : null} - {isActionsEnabled ? : null} - - ); + if (isActionsEnabled) { + return ; } + return ; }); @@ -26,8 +22,6 @@ RightButtons.propTypes = { theme: PropTypes.string, showSend: PropTypes.bool, submit: PropTypes.func.isRequired, - recordAudioMessage: PropTypes.func.isRequired, - recordAudioMessageEnabled: PropTypes.bool, showMessageBoxActions: PropTypes.func.isRequired, isActionsEnabled: PropTypes.bool }; diff --git a/app/containers/MessageBox/RightButtons.ios.js b/app/containers/MessageBox/RightButtons.ios.js index 62fbca5ee8..9ea5fc74e8 100644 --- a/app/containers/MessageBox/RightButtons.ios.js +++ b/app/containers/MessageBox/RightButtons.ios.js @@ -1,26 +1,19 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { SendButton, AudioButton } from './buttons'; +import { SendButton } from './buttons'; -const RightButtons = React.memo(({ - theme, showSend, submit, recordAudioMessage, recordAudioMessageEnabled -}) => { +const RightButtons = React.memo(({ theme, showSend, submit }) => { if (showSend) { return ; } - if (recordAudioMessageEnabled) { - return ; - } return null; }); RightButtons.propTypes = { theme: PropTypes.string, showSend: PropTypes.bool, - submit: PropTypes.func.isRequired, - recordAudioMessage: PropTypes.func.isRequired, - recordAudioMessageEnabled: PropTypes.bool + submit: PropTypes.func.isRequired }; export default RightButtons; diff --git a/app/containers/MessageBox/buttons/AudioButton.js b/app/containers/MessageBox/buttons/AudioButton.js deleted file mode 100644 index 4c63656dd8..0000000000 --- a/app/containers/MessageBox/buttons/AudioButton.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import BaseButton from './BaseButton'; - -const AudioButton = React.memo(({ theme, onPress }) => ( - -)); - -AudioButton.propTypes = { - theme: PropTypes.string, - onPress: PropTypes.func.isRequired -}; - -export default AudioButton; diff --git a/app/containers/MessageBox/buttons/index.js b/app/containers/MessageBox/buttons/index.js index 15375b9054..523764469e 100644 --- a/app/containers/MessageBox/buttons/index.js +++ b/app/containers/MessageBox/buttons/index.js @@ -1,13 +1,11 @@ import CancelEditingButton from './CancelEditingButton'; import ToggleEmojiButton from './ToggleEmojiButton'; import SendButton from './SendButton'; -import AudioButton from './AudioButton'; import ActionsButton from './ActionsButton'; export { CancelEditingButton, ToggleEmojiButton, SendButton, - AudioButton, ActionsButton }; diff --git a/app/containers/MessageBox/index.js b/app/containers/MessageBox/index.js index 87270fb0d3..813d7d64cb 100644 --- a/app/containers/MessageBox/index.js +++ b/app/containers/MessageBox/index.js @@ -17,7 +17,7 @@ import RocketChat from '../../lib/rocketchat'; import styles from './styles'; import database from '../../lib/database'; import { emojis } from '../../emojis'; -import Recording from './Recording'; +import RecordAudio from './RecordAudio'; import log from '../../utils/log'; import I18n from '../../i18n'; import ReplyPreview from './ReplyPreview'; @@ -541,12 +541,14 @@ class MessageBox extends Component { setCommandPreview = async(command, name, params) => { const { rid } = this.props; try { - const { preview } = await RocketChat.getCommandPreview(name, rid, params); - this.setState({ commandPreview: preview.items, showCommandPreview: true, command }); + const { success, preview } = await RocketChat.getCommandPreview(name, rid, params); + if (success) { + return this.setState({ commandPreview: preview?.items, showCommandPreview: true, command }); + } } catch (e) { - this.setState({ commandPreview: [], showCommandPreview: true, command: {} }); log(e); } + this.setState({ commandPreview: [], showCommandPreview: true, command: {} }); } setInput = (text) => { @@ -669,8 +671,7 @@ class MessageBox extends Component { }); } - recordAudioMessage = async() => { - const recording = await Recording.permission(); + recordingCallback = (recording) => { this.setState({ recording }); } @@ -679,9 +680,6 @@ class MessageBox extends Component { rid, tmid, baseUrl: server, user } = this.props; - this.setState({ - recording: false - }); if (fileInfo) { try { if (this.canUploadFile(fileInfo)) { @@ -844,63 +842,84 @@ class MessageBox extends Component { returnKeyType: 'send' } : {}; - if (recording) { - return ; - } - return ( + const recordAudio = showSend || !Message_AudioRecorderEnabled ? null : ( + + ); + + const commandsPreviewAndMentions = !recording ? ( <> - - + + ) : null; + + const replyPreview = !recording ? ( + + ) : null; + + const textInputAndButtons = !recording ? ( + <> + + this.component = component} + style={styles.textBoxInput} + returnKeyType='default' + keyboardType='twitter' + blurOnSubmit={false} + placeholder={I18n.t('New_Message')} + onChangeText={this.onChangeText} + underlineColorAndroid='transparent' + defaultValue='' + multiline + testID='messagebox-input' + theme={theme} + {...isAndroidTablet} + /> + + + ) : null; + + return ( + <> + {commandsPreviewAndMentions} + + {replyPreview} - - this.component = component} - style={styles.textBoxInput} - returnKeyType='default' - keyboardType='twitter' - blurOnSubmit={false} - placeholder={I18n.t('New_Message')} - onChangeText={this.onChangeText} - underlineColorAndroid='transparent' - defaultValue='' - multiline - testID='messagebox-input' - theme={theme} - {...isAndroidTablet} - /> - + {textInputAndButtons} + {recordAudio} {children} diff --git a/app/containers/MessageBox/styles.js b/app/containers/MessageBox/styles.js index f2fb9c5547..96001fc155 100644 --- a/app/containers/MessageBox/styles.js +++ b/app/containers/MessageBox/styles.js @@ -15,7 +15,7 @@ export default StyleSheet.create({ }, composer: { flexDirection: 'column', - borderTopWidth: StyleSheet.hairlineWidth + borderTopWidth: 1 }, textArea: { flexDirection: 'row', @@ -104,6 +104,15 @@ export default StyleSheet.create({ scrollViewMention: { maxHeight: SCROLLVIEW_MENTION_HEIGHT }, + recordingContent: { + flexDirection: 'row', + flex: 1, + justifyContent: 'space-between' + }, + recordingCancelText: { + fontSize: 17, + ...sharedStyles.textRegular + }, buttonsWhitespace: { width: 15 } diff --git a/app/containers/SearchBox.js b/app/containers/SearchBox.js index 7c3e337ee1..89c51def1a 100644 --- a/app/containers/SearchBox.js +++ b/app/containers/SearchBox.js @@ -47,7 +47,7 @@ const styles = StyleSheet.create({ const CancelButton = (onCancelPress, theme) => ( - {I18n.t('Cancel')} + {I18n.t('Cancel')} ); diff --git a/app/containers/StatusBar.js b/app/containers/StatusBar.js index cc9bd73cc1..8add422f1a 100644 --- a/app/containers/StatusBar.js +++ b/app/containers/StatusBar.js @@ -2,13 +2,12 @@ import React from 'react'; import { StatusBar as StatusBarRN } from 'react-native'; import PropTypes from 'prop-types'; -import { isIOS } from '../utils/deviceInfo'; import { themes } from '../constants/colors'; const StatusBar = React.memo(({ theme, barStyle, backgroundColor }) => { if (!barStyle) { barStyle = 'light-content'; - if (theme === 'light' && isIOS) { + if (theme === 'light') { barStyle = 'dark-content'; } } diff --git a/app/containers/message/Audio.js b/app/containers/message/Audio.js index 27a6d08eee..74410fa3f8 100644 --- a/app/containers/message/Audio.js +++ b/app/containers/message/Audio.js @@ -126,7 +126,6 @@ class MessageAudio extends React.Component { this.setState({ loading: true }); try { - await Audio.setAudioModeAsync(mode); await this.sound.loadAsync({ uri: `${ url }?rc_uid=${ user.id }&rc_token=${ user.token }` }); } catch { // Do nothing @@ -225,6 +224,7 @@ class MessageAudio extends React.Component { if (paused) { await this.sound.pauseAsync(); } else { + await Audio.setAudioModeAsync(mode); await this.sound.playAsync(); } } catch { diff --git a/app/i18n/locales/de.js b/app/i18n/locales/de.js index bc74bbdd24..0d01883799 100644 --- a/app/i18n/locales/de.js +++ b/app/i18n/locales/de.js @@ -27,9 +27,9 @@ export default { 'error-invalid-arguments': 'Ungültige Argumente', 'error-invalid-asset': 'Ungültiges Asset', 'error-invalid-channel': 'Ungültiger Kanal', - 'error-invalid-channel-start-with-chars': 'Ungültiger Kanal. Beginnen Sie mit @ oder #', + 'error-invalid-channel-start-with-chars': 'Ungültiger Kanal. Beginne mit @ oder #', 'error-invalid-custom-field': 'Ungültiges benutzerdefiniertes Feld', - 'error-invalid-custom-field-name': 'Ungültiger benutzerdefinierter Feldname. Verwenden Sie nur Buchstaben, Zahlen, Bindestriche und Unterstriche.', + 'error-invalid-custom-field-name': 'Ungültiger benutzerdefinierter Feldname. Verwende nur Buchstaben, Zahlen, Bindestriche und Unterstriche.', 'error-invalid-date': 'Ungültiges Datum angegeben', 'error-invalid-description': 'Ungültige Beschreibung', 'error-invalid-domain': 'Ungültige Domain', @@ -38,7 +38,7 @@ export default { 'error-invalid-file-height': 'Ungültige Dateihöhe', 'error-invalid-file-type': 'Ungültiger Dateityp', 'error-invalid-file-width': 'Ungültige Dateibreite', - 'error-invalid-from-address': 'Sie haben eine ungültige FROM-Adresse mitgeteilt.', + 'error-invalid-from-address': 'Du hast eine ungültige FROM-Adresse mitgeteilt.', 'error-invalid-integration': 'Ungültige Integration', 'error-invalid-message': 'Ungültige Nachricht', 'error-invalid-method': 'Ungültige Methode', @@ -60,24 +60,24 @@ export default { 'error-message-deleting-blocked': 'Das Löschen von Nachrichten ist gesperrt', 'error-message-editing-blocked': 'Die Bearbeitung von Nachrichten ist gesperrt', 'error-message-size-exceeded': 'Die Nachrichtengröße überschreitet Message_MaxAllowedSize', - 'error-missing-unsubscribe-link': 'Sie müssen den Link [abbestellen] angeben.', + 'error-missing-unsubscribe-link': 'Du musst den Link [abbestellen] angeben.', 'error-no-tokens-for-this-user': 'Für diesen Benutzer gibt es keine Token', 'error-not-allowed': 'Nicht erlaubt', 'error-not-authorized': 'Nicht berechtigt', 'error-push-disabled': 'Push ist deaktiviert', - 'error-remove-last-owner': 'Dies ist der letzte Besitzer. Bitte legen Sie einen neuen Besitzer fest, bevor Sie diesen entfernen.', + 'error-remove-last-owner': 'Dies ist der letzte Besitzer. Bitte lege einen neuen Besitzer fest, bevor du diesen entfernst.', 'error-role-in-use': 'Rolle kann nicht gelöscht werden, da sie gerade verwendet wird', 'error-role-name-required': 'Der Rollenname ist erforderlich', 'error-the-field-is-required': 'Das Feld {{field}} ist erforderlich.', - 'error-too-many-requests': 'Fehler, zu viele Anfragen. Sie müssen {{Sekunden}} Sekunden warten, bevor Sie es erneut versuchen.', + 'error-too-many-requests': 'Fehler, zu viele Anfragen. Du musst {{Sekunden}} Sekunden warten, bevor du es erneut versuchst.', 'error-user-is-not-activated': 'Benutzer ist nicht aktiviert', 'error-user-has-no-roles': 'Benutzer hat keine Rollen', - 'error-user-limit-exceeded': 'Die Anzahl der Benutzer, die Sie zu #channel_name einladen möchten, überschreitet die vom Administrator festgelegte Grenze', + 'error-user-limit-exceeded': 'Die Anzahl der Benutzer, die du zu #channel_name einladen möchtest, überschreitet die vom Administrator festgelegte Grenze', 'error-user-not-in-room': 'Benutzer ist nicht in diesem Raum', 'error-user-registration-custom-field': 'error-user-registration-custom-field', 'error-user-registration-disabled': 'Die Benutzerregistrierung ist deaktiviert', 'error-user-registration-secret': 'Die Benutzerregistrierung ist nur über eine geheime URL möglich', - 'error-you-are-last-owner': 'Sie sind der letzte Besitzer. Bitte setzen Sie einen neuen Besitzer, bevor Sie den Raum verlassen.', + 'error-you-are-last-owner': 'Du bist der letzte Besitzer. Bitte setze einen neuen Besitzer, bevor du den Raum verlässt.', Actions: 'Aktionen', activity: 'Aktivität', Activity: 'Aktivität', @@ -99,13 +99,13 @@ export default { and: 'und', announcement: 'Ankündigung', Announcement: 'Ankündigung', - Apply_Your_Certificate: 'Wenden Sie Ihr Zertifikat an', + Apply_Your_Certificate: 'Wende dein Zertifikat an', Applying_a_theme_will_change_how_the_app_looks: 'Das Erscheinungsbild festzulegen wird das Aussehen der Anwendung ändern.', ARCHIVE: 'ARCHIV', archive: 'Archiv', are_typing: 'tippen', Are_you_sure_question_mark: 'Bist du sicher?', - Are_you_sure_you_want_to_leave_the_room: 'Möchten Sie den Raum wirklich verlassen {{room}}?', + Are_you_sure_you_want_to_leave_the_room: 'Möchtest du den Raum wirklich verlassen {{room}}?', Audio: 'Audio', Authenticating: 'Authentifizierung', Automatic: 'Automatisch', @@ -120,7 +120,7 @@ export default { Broadcast_channel_Description: 'Nur autorisierte Benutzer können neue Nachrichten schreiben, die anderen Benutzer können jedoch antworten', Broadcast_Channel: 'Broadcastkanal', Busy: 'Beschäftigt', - By_proceeding_you_are_agreeing: 'Indem Sie fortfahren, stimmen Sie zu unserem', + By_proceeding_you_are_agreeing: 'Indem du fortfährst, stimmst du zu unserem', Cancel_editing: 'Bearbeitung abbrechen', Cancel_recording: 'Aufnahme abbrechen', Cancel: 'Abbrechen', @@ -133,7 +133,7 @@ export default { Call_already_ended: 'Anruf bereits beendet!', Click_to_join: 'Klicken um teilzunehmen!', Close: 'Schließen', - Close_emoji_selector: 'Schließen Sie die Emoji-Auswahl', + Close_emoji_selector: 'Schließe die Emoji-Auswahl', Closing_chat: 'Chat schließen', Change_language_loading: 'Ändere Sprache.', Chat_closed_by_agent: 'Chat durch den Agenten geschlossen', @@ -150,7 +150,7 @@ export default { connecting_server: 'verbinde zum Server', Connecting: 'Verbinden ...', Contact_us: 'Kontaktiere uns', - Contact_your_server_admin: 'Kontaktieren Sie Ihren Server-Administrator.', + Contact_your_server_admin: 'Kontaktiere deinen Server-Administrator.', Continue_with: 'Weitermachen mit', Copied_to_clipboard: 'In die Zwischenablage kopiert!', Copy: 'Kopieren', @@ -159,13 +159,13 @@ export default { Certificate_password: 'Zertifikats-Passwort', Clear_cache: 'Lokalen Server-Cache leeren', Clear_cache_loading: 'Leere Cache.', - Whats_the_password_for_your_certificate: 'Wie lautet das Passwort für Ihr Zertifikat?', + Whats_the_password_for_your_certificate: 'Wie lautet das Passwort für dein Zertifikat?', Create_account: 'Ein Konto erstellen', Create_Channel: 'Kanal erstellen', Create_Direct_Messages: 'Direkt-Nachricht erstellen', Create_Discussion: 'Diskussion erstellen', Created_snippet: 'Erstellt ein Snippet', - Create_a_new_workspace: 'Erstellen Sie einen neuen Arbeitsbereich', + Create_a_new_workspace: 'Erstelle einen neuen Arbeitsbereich', Create: 'Erstellen', Custom_Status: 'eigener Status', Dark: 'Dunkel', @@ -190,8 +190,8 @@ export default { Done: 'Erledigt', Dont_Have_An_Account: 'Du hast noch kein Konto?', Do_you_have_an_account: 'Du hast schon ein Konto?', - Do_you_have_a_certificate: 'Haben Sie ein Zertifikat?', - Do_you_really_want_to_key_this_room_question_mark: 'Möchten Sie diesen Raum wirklich {{key}}?', + Do_you_have_a_certificate: 'Hast du ein Zertifikat?', + Do_you_really_want_to_key_this_room_question_mark: 'Möchtest du diesen Raum wirklich {{key}}?', edit: 'bearbeiten', edited: 'bearbeitet', Edit: 'Bearbeiten', @@ -212,10 +212,10 @@ export default { Files: 'Dateien', File_description: 'Dateibeschreibung', File_name: 'Dateiname', - Finish_recording: 'Beenden Sie die Aufnahme', + Finish_recording: 'Beende die Aufnahme', Following_thread: 'Thread folgen', - For_your_security_you_must_enter_your_current_password_to_continue: 'Zu Ihrer Sicherheit müssen Sie Ihr aktuelles Passwort eingeben, um fortzufahren', - Forgot_password_If_this_email_is_registered: 'Wenn diese E-Mail registriert ist, senden wir Anweisungen zum Zurücksetzen Ihres Passworts. Wenn Sie in Kürze keine E-Mail erhalten, kommen Sie bitte zurück und versuchen Sie es erneut.', + For_your_security_you_must_enter_your_current_password_to_continue: 'Zu deiner Sicherheit musst du dein aktuelles Passwort eingeben, um fortzufahren', + Forgot_password_If_this_email_is_registered: 'Wenn diese E-Mail registriert ist, senden wir Anweisungen zum Zurücksetzen deines Passworts. Wenn du nicht in Kürze keine E-Mail erhältst, versuche es bitte erneut.', Forgot_password: 'Passwort vergessen', Forgot_Password: 'Passwort vergessen', Forward: 'Weiterleiten', @@ -255,7 +255,7 @@ export default { is_not_a_valid_RocketChat_instance: 'ist keine gültige Rocket.Chat-Instanz', is_typing: 'schreibt', Invalid_or_expired_invite_token: 'Ungültiger oder abgelaufener Einladungscode', - Invalid_server_version: 'Der Server, zu dem Sie eine Verbindung herstellen möchten, verwendet eine Version, die von der App nicht mehr unterstützt wird: {{currentVersion}}.\n\nWir benötigen Version {{MinVersion}}.', + Invalid_server_version: 'Der Server, zu dem du dich verbinden möchtest, verwendet eine Version, die von der App nicht mehr unterstützt wird: {{currentVersion}}.\n\nWir benötigen Version {{MinVersion}}.', Invite_Link: 'Einladungs-Link', Invite_users: 'Benutzer einladen', Join: 'Beitreten', @@ -273,7 +273,7 @@ export default { Livechat: 'Live-Chat', Livechat_edit: 'Livechat bearbeiten', Login: 'Anmeldung', - Login_error: 'Ihre Zugangsdaten wurden abgelehnt! Bitte versuchen Sie es erneut.', + Login_error: 'Deine Zugangsdaten wurden abgelehnt! Bitte versuche es erneut.', Login_with: 'Einloggen mit', Logging_out: 'Abmelden.', Logout: 'Abmelden', @@ -295,7 +295,7 @@ export default { Message: 'Nachricht', Messages: 'Mitteilungen', Message_Reported: 'Nachricht gemeldet', - Microphone_Permission_Message: 'Rocket.Chat benötigt Zugriff auf Ihr Mikrofon, damit Sie eine Audionachricht senden können.', + Microphone_Permission_Message: 'Rocket.Chat benötigt Zugriff auf das Mikrofon, damit du eine Audionachricht senden kannst.', Microphone_Permission: 'Mikrofonberechtigung', Mute: 'Stumm', muted: 'stummgeschaltet', @@ -327,13 +327,14 @@ export default { Nothing: 'Nichts', Nothing_to_save: 'Nichts zu speichern!', Notify_active_in_this_room: 'Aktive Benutzer in diesem Raum benachrichtigen', - Notify_all_in_this_room: 'Benachrichtigen Sie alle in diesem Raum', + Notify_all_in_this_room: 'Benachrichtige alle in diesem Raum', Notifications: 'Benachrichtigungen', Notification_Duration: 'Benachrichtigungsdauer', Notification_Preferences: 'Benachrichtigungseinstellungen', No_available_agents_to_transfer: 'Keine Agenten für den Transfer verfügbar', Offline: 'Offline', Oops: 'Hoppla!', + Omnichannel: 'Omnichannel', Onboarding_description: 'Ein Arbeitsbereich ist der Ort für die Zusammenarbeit deines Teams oder Organisation. Bitte den Admin des Arbeitsbereichs um eine Adresse, um ihm beizutreten, oder erstelle einen Arbeitsbereich für dein Team.', Onboarding_join_workspace: 'Tritt einem Arbeitsbereich bei', Onboarding_subtitle: 'Mehr als Team-Zusammenarbeit', @@ -359,7 +360,7 @@ export default { pinned: 'angeheftet', Pinned: 'Angeheftet', Please_add_a_comment: 'Bitte Kommentar hinzufügen', - Please_enter_your_password: 'Bitte geben Sie Ihr Passwort ein', + Please_enter_your_password: 'Gib bitte dein Passwort ein', Please_wait: 'Bitte warten.', Preferences: 'Einstellungen', Preferences_saved: 'Einstellungen gespeichert!', @@ -379,6 +380,8 @@ export default { Reactions_are_enabled: 'Reaktionen sind aktiviert', Reactions: 'Reaktionen', Read: 'Gelesen', + Read_External_Permission_Message: 'Rocket.Chat benötigt Zugriff auf deine Fotos, Medien und Dateien auf deinem Gerät', + Read_External_Permission: 'Lese-Zugriff auf Medien', Read_Only_Channel: 'Nur-Lese-Kanal', Read_Only: 'Schreibgeschützt', Read_Receipt: 'Lesebestätigung', @@ -405,6 +408,7 @@ export default { Review_app_later: 'Vielleicht später', Review_app_unable_store: 'Kann {{store}} nicht öffnen', Review_this_app: 'App bewerten', + Remove: 'Entfernen', Roles: 'Rollen', Room_actions: 'Raumaktionen', Room_changed_announcement: 'Raumansage geändert in: {{announcement}} von {{userBy}}', @@ -428,9 +432,9 @@ export default { Search: 'Suche', Search_by: 'Suche nach', Search_global_users: 'Suche nach globalen Benutzern', - Search_global_users_description: 'Beim Einschalten können Sie nach Benutzern von anderen Unternehmen oder Servern suchen.', + Search_global_users_description: 'Wenn aktiviert, kannst du nach Benutzern von anderen Unternehmen oder Servern suchen.', Seconds: '{{second}} Sekunden', - Select_Avatar: 'Wählen Sie einen Avatar aus', + Select_Avatar: 'Wähle einen Avatar aus', Select_Server: 'Server auswählen', Select_Users: 'Benutzer auswählen', Select_a_Channel: 'Kanal auswählen', @@ -443,11 +447,12 @@ export default { Send_message: 'Nachricht senden', Send_me_the_code_again: 'Den Code neu versenden', Send_to: 'Senden an …', + Sending_to: 'Sende an', Sent_an_attachment: 'Sende einen Anhang', Server: 'Server', Servers: 'Server', Server_version: 'Server version: {{version}}', - Set_username_subtitle: 'Der Benutzername wird verwendet, damit andere Personen Sie in Nachrichten erwähnen können', + Set_username_subtitle: 'Der Benutzername wird verwendet, damit andere Personen dich in Nachrichten erwähnen können', Set_custom_status: 'Individuellen Status setzen', Set_status: 'Status setzen', Status_saved_successfully: 'Status erfolgreich gesetzt!', @@ -470,6 +475,7 @@ export default { starred: 'favorisiert', Starred: 'Favorisiert', Start_of_conversation: 'Beginn des Gesprächs', + Start_a_Discussion: 'Beginne eine Diskussion', Started_discussion: 'Hat eine Diskussion gestartet:', Started_call: 'Anruf gestartet von {{userBy}}', Submit: 'einreichen', @@ -478,10 +484,12 @@ export default { Take_a_photo: 'Foto aufnehmen', Take_a_video: 'Video aufnehmen', tap_to_change_status: 'Tippen um den Status zu ändern', - Tap_to_view_servers_list: 'Tippen Sie hier, um die Serverliste anzuzeigen', + Tap_to_view_servers_list: 'Hier tippen, um die Serverliste anzuzeigen', Terms_of_Service: ' Nutzungsbedingungen', Theme: 'Erscheinungsbild', - The_URL_is_invalid: 'Die eingegebene URL ist ungültig. Überprüfen Sie es und versuchen Sie es bitte erneut!', + The_URL_is_invalid: 'Die eingegebene URL ist ungültig. Überprüfe sie bitte noch einmal und versuche es erneut!', + The_user_wont_be_able_to_type_in_roomName: 'Dem Nutzer wird es nicht möglich sein in {{roomName}} zu schreiben', + The_user_will_be_able_to_type_in_roomName: 'Der Nutzer wird in {{roomName}} schreiben können', There_was_an_error_while_action: 'Während {{action}} ist ein Fehler aufgetreten!', This_room_is_blocked: 'Dieser Raum ist gesperrt', This_room_is_read_only: 'Dieser Raum kann nur gelesen werden', @@ -492,7 +500,7 @@ export default { topic: 'Thema', Topic: 'Thema', Translate: 'Übersetzen', - Try_again: 'Versuchen Sie es nochmal', + Try_again: 'Versuche es nochmal', Two_Factor_Authentication: 'Zwei-Faktor-Authentifizierung', Type_the_channel_name_here: 'Gib hier den Kanalnamen ein', unarchive: 'wiederherstellen', @@ -535,28 +543,29 @@ export default { Video_call: 'Videoanruf', View_Original: 'Original anzeigen', Voice_call: 'Sprachanruf', + Waiting_for_network: 'Warte auf das Netzwerk …', Websocket_disabled: 'Websockets sind auf diesem Server nicht aktiviert.\n{{contact}}', Welcome: 'Herzlich willkommen', What_are_you_doing_right_now: 'Was machst du gerade?', - Whats_your_2fa: 'Wie lautet Ihr 2FA-Code?', + Whats_your_2fa: 'Wie lautet dein 2FA-Code?', Without_Servers: 'Ohne Server', Workspaces: 'Arbeitsbereiche', Would_you_like_to_return_the_inquiry: 'Willst du zur Anfrage zurück?', - Write_External_Permission_Message: 'Rocket.Chat benötigt Zugriff auf Ihre Galerie um Bilder speichern zu können.', + Write_External_Permission_Message: 'Rocket.Chat benötigt Zugriff auf deine Galerie um Bilder speichern zu können.', Write_External_Permission: 'Galerie-Zugriff', Yes: 'Ja', Yes_action_it: 'Ja, {{action}}!', Yesterday: 'Gestern', - You_are_in_preview_mode: 'Sie befinden sich im Vorschaumodus', - You_are_offline: 'Sie sind offline', - You_can_search_using_RegExp_eg: 'Sie können mit RegExp suchen. z.B. `/ ^ text $ / i`', - You_colon: 'Sie: ', - you_were_mentioned: 'Sie wurden erwähnt', - You_were_removed_from_channel: 'Sie wurden aus dem Kanal {{channel}} entfernt', - you: 'Sie', - You: 'Sie', + You_are_in_preview_mode: 'Du befindest dich im Vorschaumodus', + You_are_offline: 'Du bist offline', + You_can_search_using_RegExp_eg: 'Du kannst mit RegExp suchen. z.B. `/ ^ text $ / i`', + You_colon: 'Du: ', + you_were_mentioned: 'Du wurdest erwähnt', + You_were_removed_from_channel: 'Du wurdest aus dem Kanal {{channel}} entfernt', + you: 'du', + You: 'Du', Logged_out_by_server: 'Du bist vom Server abgemeldet worden. Bitte melde dich wieder an.', - You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'Sie benötigen Zugang zu mindestens einem Rocket.Chat-Server um etwas zu teilen.', + You_need_to_access_at_least_one_RocketChat_server_to_share_something: 'Du benötigst Zugang zu mindestens einem Rocket.Chat-Server um etwas zu teilen.', Your_certificate: 'Dein Zertifikat', Your_message: 'Deine Nachricht', Your_invite_link_will_expire_after__usesLeft__uses: 'Dein Einladungs-Link wird nach {{usesLeft}} Benutzungen ablaufen.', @@ -565,9 +574,10 @@ export default { Your_invite_link_will_never_expire: 'Dein Einladungs-Link wird niemals ablaufen.', Your_workspace: 'Dein Arbeitsbereich', Version_no: 'Version: {{version}}', - You_will_not_be_able_to_recover_this_message: 'Sie können diese Nachricht nicht wiederherstellen!', + You_will_not_be_able_to_recover_this_message: 'Du kannst diese Nachricht nicht wiederherstellen!', + You_will_unset_a_certificate_for_this_server: 'Du entfernst ein Zertifikat für diesen Server', Change_Language: 'Sprache ändern', - Crash_report_disclaimer: 'Wir verfolgen niemals den Inhalt Ihrer Chats. Der Crash-Report enthält nur für uns relevante Informationen um das Problem zu erkennen und zu beheben.', + Crash_report_disclaimer: 'Wir verfolgen niemals den Inhalt deiner Chats. Der Crash-Report enthält nur für uns relevante Informationen um das Problem zu erkennen und zu beheben.', Type_message: 'Type message', Room_search: 'Raum-Suche', Room_selection: 'Raum-Auswahl 1...9', @@ -578,6 +588,7 @@ export default { Search_messages: 'Nachrichten durchsuchen', Scroll_messages: 'Nachrichten durchblättern', Reply_latest: 'Auf die letzte Nachricht antworten', + Reply_in_Thread: 'Im Thread antworten', Server_selection: 'Server-Auswahl', Server_selection_numbers: 'Server-Auswahl 1...9', Add_server: 'Server hinufügen', diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index c973245bee..e6038732eb 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -295,7 +295,7 @@ export default { Message: 'Message', Messages: 'Messages', Message_Reported: 'Message reported', - Microphone_Permission_Message: 'Rocket Chat needs access to your microphone so you can send audio message.', + Microphone_Permission_Message: 'Rocket.Chat needs access to your microphone so you can send audio message.', Microphone_Permission: 'Microphone Permission', Mute: 'Mute', muted: 'muted', @@ -380,7 +380,7 @@ export default { Reactions_are_enabled: 'Reactions are enabled', Reactions: 'Reactions', Read: 'Read', - Read_External_Permission_Message: 'Rocket Chat needs to access photos, media, and files on your device', + Read_External_Permission_Message: 'Rocket.Chat needs to access photos, media, and files on your device', Read_External_Permission: 'Read Media Permission', Read_Only_Channel: 'Read Only Channel', Read_Only: 'Read Only', @@ -543,6 +543,7 @@ export default { Video_call: 'Video call', View_Original: 'View Original', Voice_call: 'Voice call', + Waiting_for_network: 'Waiting for network...', Websocket_disabled: 'Websocket is disabled for this server.\n{{contact}}', Welcome: 'Welcome', What_are_you_doing_right_now: 'What are you doing right now?', @@ -550,7 +551,7 @@ export default { Without_Servers: 'Without Servers', Workspaces: 'Workspaces', Would_you_like_to_return_the_inquiry: 'Would you like to return the inquiry?', - Write_External_Permission_Message: 'Rocket Chat needs access to your gallery so you can save images.', + Write_External_Permission_Message: 'Rocket.Chat needs access to your gallery so you can save images.', Write_External_Permission: 'Gallery Permission', Yes: 'Yes', Yes_action_it: 'Yes, {{action}} it!', diff --git a/app/i18n/locales/es-ES.js b/app/i18n/locales/es-ES.js index c57f6800a8..74993266d3 100644 --- a/app/i18n/locales/es-ES.js +++ b/app/i18n/locales/es-ES.js @@ -233,7 +233,7 @@ export default { messages: 'mensajes', Messages: 'Mensajes', Message_Reported: 'Mensaje notificado', - Microphone_Permission_Message: 'Rocket Chat necesita acceso a su micrófono para que pueda enviar un mensaje de audio.', + Microphone_Permission_Message: 'Rocket.Chat necesita acceso a su micrófono para que pueda enviar un mensaje de audio.', Microphone_Permission: 'Permiso de micrófono', Mute: 'Mutear', muted: 'muteado', diff --git a/app/i18n/locales/fr.js b/app/i18n/locales/fr.js index a2f8fe008a..ce638a0dbf 100644 --- a/app/i18n/locales/fr.js +++ b/app/i18n/locales/fr.js @@ -194,7 +194,7 @@ export default { Message_pinned: 'Message épinglé', Message_removed: 'Message supprimé', Messages: 'Messages', - Microphone_Permission_Message: 'Rocket Chat doit avoir accès à votre microphone pour pouvoir envoyer un message audio.', + Microphone_Permission_Message: 'Rocket.Chat doit avoir accès à votre microphone pour pouvoir envoyer un message audio.', Microphone_Permission: 'Permission de microphone', Mute: 'Rendre muet', muted: 'Rendu muet', diff --git a/app/i18n/locales/ja.js b/app/i18n/locales/ja.js index 30090f6354..b3cc25a544 100644 --- a/app/i18n/locales/ja.js +++ b/app/i18n/locales/ja.js @@ -232,8 +232,8 @@ export default { 'アプリを表示中にはバナーを上部に表示し、デスクトップには通知を送ります。', Invisible: '状態を隠す', Invite: '招待', - is_a_valid_RocketChat_instance: 'は正しいRocket Chatのインスタンスです', - is_not_a_valid_RocketChat_instance: 'はRocket Chatのインスタンスではありません', + is_a_valid_RocketChat_instance: 'は正しいRocket.Chatのインスタンスです', + is_not_a_valid_RocketChat_instance: 'はRocket.Chatのインスタンスではありません', is_typing: 'が入力中', Invalid_or_expired_invite_token: '招待トークンが無効か、期限が切れています', Invalid_server_version: @@ -272,7 +272,7 @@ export default { Messages: 'メッセージ', Message_Reported: 'メッセージを報告しました', Microphone_Permission_Message: - 'Rocket Chatは音声メッセージを送信するのにマイクのアクセスの許可が必要です。', + 'Rocket.Chatは音声メッセージを送信するのにマイクのアクセスの許可が必要です。', Microphone_Permission: 'マイクの許可', Mute: 'ミュート', muted: 'ミュートした', @@ -298,7 +298,7 @@ export default { No_Reactions: 'リアクションなし', No_Read_Receipts: '未読通知はありません', Not_logged: 'ログされていません', - Not_RC_Server: 'Rocket Chatサーバーではありません。\n{{contact}}', + Not_RC_Server: 'Rocket.Chatサーバーではありません。\n{{contact}}', Nothing: '何もなし', Nothing_to_save: '保存するものはありません!', Notify_active_in_this_room: 'このルームのアクティブなユーザーに通知する', @@ -488,7 +488,7 @@ export default { Whats_your_2fa: '2段階認証のコードを入力してください', Without_Servers: 'サーバーを除く', Write_External_Permission_Message: - 'Rocket Chatは画像を保存するためにギャラリーへのアクセスを求めています。', + 'Rocket.Chatは画像を保存するためにギャラリーへのアクセスを求めています。', Write_External_Permission: 'ギャラリーへのアクセス許可', Yes_action_it: 'はい、{{action}}します!', Yesterday: '昨日', diff --git a/app/i18n/locales/nl.js b/app/i18n/locales/nl.js index fb41fdfb62..39cecb99c9 100644 --- a/app/i18n/locales/nl.js +++ b/app/i18n/locales/nl.js @@ -242,7 +242,7 @@ export default { messages: 'berichten', Messages: 'Berichten', Message_Reported: 'Bericht gerapporteerd', - Microphone_Permission_Message: 'Rocket Chat heeft toegang tot je microfoon nodig voor geluidsberichten.', + Microphone_Permission_Message: 'Rocket.Chat heeft toegang tot je microfoon nodig voor geluidsberichten.', Microphone_Permission: 'Microfoon toestemming', Mute: 'Dempen', muted: 'gedempt', @@ -448,7 +448,7 @@ export default { Welcome: 'Welkom', Whats_your_2fa: 'Wat is je 2FA code?', Without_Servers: 'Zonder Servers', - Write_External_Permission_Message: 'Rocket Chat moet bij je galerij kunnen om afbeeldingen op te slaan.', + Write_External_Permission_Message: 'Rocket.Chat moet bij je galerij kunnen om afbeeldingen op te slaan.', Write_External_Permission: 'Galerij Toestemming', Yes_action_it: 'Ja, {{action}} het!', Yesterday: 'Gisteren', diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index 8055e7ea3e..5e1d2342ba 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -271,7 +271,7 @@ export default { message: 'mensagem', messages: 'mensagens', Messages: 'Mensagens', - Microphone_Permission_Message: 'Rocket Chat precisa de acesso ao seu microfone para enviar mensagens de áudio.', + Microphone_Permission_Message: 'Rocket.Chat precisa de acesso ao seu microfone para enviar mensagens de áudio.', Microphone_Permission: 'Acesso ao Microfone', Mute: 'Mudo', muted: 'mudo', @@ -344,7 +344,7 @@ export default { Reactions_are_disabled: 'Reagir está desabilitado', Reactions_are_enabled: 'Reagir está habilitado', Reactions: 'Reações', - Read_External_Permission_Message: 'Rocket Chat precisa acessar fotos, mídia e arquivos no seu dispositivo', + Read_External_Permission_Message: 'Rocket.Chat precisa acessar fotos, mídia e arquivos no seu dispositivo', Read_External_Permission: 'Permissão de acesso à arquivos', Read_Only_Channel: 'Canal Somente Leitura', Read_Only: 'Somente Leitura', @@ -479,6 +479,7 @@ export default { Verify_your_email_for_the_code_we_sent: 'Verifique em seu e-mail o código que enviamos', Video_call: 'Chamada de vídeo', Voice_call: 'Chamada de voz', + Waiting_for_network: 'Aguardando rede...', Websocket_disabled: 'Websocket está desativado para esse servidor.\n{{contact}}', Welcome: 'Bem vindo', Whats_your_2fa: 'Qual seu código de autenticação?', @@ -504,7 +505,7 @@ export default { You_will_not_be_able_to_recover_this_message: 'Você não será capaz de recuperar essa mensagem!', You_will_unset_a_certificate_for_this_server: 'Você cancelará a configuração de um certificado para este servidor', Would_you_like_to_return_the_inquiry: 'Deseja retornar a consulta?', - Write_External_Permission_Message: 'Rocket Chat precisa de acesso à sua galeria para salvar imagens', + Write_External_Permission_Message: 'Rocket.Chat precisa de acesso à sua galeria para salvar imagens', Write_External_Permission: 'Acesso à Galeria', Yes: 'Sim', Crash_report_disclaimer: 'Nós não rastreamos o conteúdo das suas conversas. O relatório de erros apenas contém informações relevantes para identificarmos problemas e corrigí-los.', diff --git a/app/i18n/locales/pt-PT.js b/app/i18n/locales/pt-PT.js index 7de141116a..a702951bd7 100644 --- a/app/i18n/locales/pt-PT.js +++ b/app/i18n/locales/pt-PT.js @@ -195,7 +195,7 @@ export default { Message_pinned: 'Mensagem afixada', Message_removed: 'Mensagem removida', Messages: 'Mensagens', - Microphone_Permission_Message: 'O Rocket Chat necessita de acesso ao seu microfone para que você possa enviar mensagens de áudio.', + Microphone_Permission_Message: 'O Rocket.Chat necessita de acesso ao seu microfone para que você possa enviar mensagens de áudio.', Microphone_Permission: 'Permissão de Microfone', Mute: 'Silenciar', muted: 'silenciado', diff --git a/app/i18n/locales/ru.js b/app/i18n/locales/ru.js index f2d45ef2e3..53579a7cd5 100644 --- a/app/i18n/locales/ru.js +++ b/app/i18n/locales/ru.js @@ -227,7 +227,7 @@ export default { messages: 'сообщения', Messages: 'Сообщения', Message_Reported: 'Сообщение отправлено', - Microphone_Permission_Message: 'Rocket Chat нужен доступ к вашему микрофону, чтобы вы могли отправлять аудиосообщения.', + Microphone_Permission_Message: 'Rocket.Chat нужен доступ к вашему микрофону, чтобы вы могли отправлять аудиосообщения.', Microphone_Permission: 'Разрешение на использование микрофона', Mute: 'Заглушить', muted: 'Заглушен', diff --git a/app/i18n/locales/zh-CN.js b/app/i18n/locales/zh-CN.js index fa0b9c97b8..068125219f 100644 --- a/app/i18n/locales/zh-CN.js +++ b/app/i18n/locales/zh-CN.js @@ -190,7 +190,7 @@ export default { Message_pinned: '消息被钉住', Message_removed: '消息被删除', Messages: '消息', - Microphone_Permission_Message: 'Rocket Chat需要访问您的麦克风,以便您可以发送音频消息。', + Microphone_Permission_Message: 'Rocket.Chat需要访问您的麦克风,以便您可以发送音频消息。', Microphone_Permission: '麦克风授权', Mute: '静音', muted: '被静音', diff --git a/app/lib/database/index.js b/app/lib/database/index.js index 31bf5317b6..37a17a3cd2 100644 --- a/app/lib/database/index.js +++ b/app/lib/database/index.js @@ -110,7 +110,9 @@ class DB { Thread, ThreadMessage, Upload, - Permission + Permission, + CustomEmoji, + FrequentlyUsedEmoji ], actionsEnabled: true }); diff --git a/app/lib/methods/getSettings.js b/app/lib/methods/getSettings.js index 7a6e351cf9..e635807e9f 100644 --- a/app/lib/methods/getSettings.js +++ b/app/lib/methods/getSettings.js @@ -1,4 +1,3 @@ -import { InteractionManager } from 'react-native'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { Q } from '@nozbe/watermelondb'; @@ -132,48 +131,47 @@ export default async function() { const filteredSettingsIds = filteredSettings.map(s => s._id); reduxStore.dispatch(addSettings(this.parseSettings(filteredSettings))); - InteractionManager.runAfterInteractions(async() => { - // filter server info - const serverInfo = filteredSettings.filter(i1 => serverInfoKeys.includes(i1._id)); - const iconSetting = data.find(item => item._id === 'Assets_favicon_512'); - await serverInfoUpdate(serverInfo, iconSetting); - - await db.action(async() => { - const settingsCollection = db.collections.get('settings'); - const allSettingsRecords = await settingsCollection - .query(Q.where('id', Q.oneOf(filteredSettingsIds))) - .fetch(); - - // filter settings - let settingsToCreate = filteredSettings.filter(i1 => !allSettingsRecords.find(i2 => i1._id === i2.id)); - let settingsToUpdate = allSettingsRecords.filter(i1 => filteredSettings.find(i2 => i1.id === i2._id)); - - // Create - settingsToCreate = settingsToCreate.map(setting => settingsCollection.prepareCreate(protectedFunction((s) => { - s._raw = sanitizedRaw({ id: setting._id }, settingsCollection.schema); - Object.assign(s, setting); - }))); - - // Update - settingsToUpdate = settingsToUpdate.map((setting) => { - const newSetting = filteredSettings.find(s => s._id === setting.id); - return setting.prepareUpdate(protectedFunction((s) => { - Object.assign(s, newSetting); - })); - }); - - const allRecords = [ - ...settingsToCreate, - ...settingsToUpdate - ]; - - try { - await db.batch(...allRecords); - } catch (e) { - log(e); - } - return allRecords.length; + + // filter server info + const serverInfo = filteredSettings.filter(i1 => serverInfoKeys.includes(i1._id)); + const iconSetting = data.find(item => item._id === 'Assets_favicon_512'); + await serverInfoUpdate(serverInfo, iconSetting); + + await db.action(async() => { + const settingsCollection = db.collections.get('settings'); + const allSettingsRecords = await settingsCollection + .query(Q.where('id', Q.oneOf(filteredSettingsIds))) + .fetch(); + + // filter settings + let settingsToCreate = filteredSettings.filter(i1 => !allSettingsRecords.find(i2 => i1._id === i2.id)); + let settingsToUpdate = allSettingsRecords.filter(i1 => filteredSettings.find(i2 => i1.id === i2._id)); + + // Create + settingsToCreate = settingsToCreate.map(setting => settingsCollection.prepareCreate(protectedFunction((s) => { + s._raw = sanitizedRaw({ id: setting._id }, settingsCollection.schema); + Object.assign(s, setting); + }))); + + // Update + settingsToUpdate = settingsToUpdate.map((setting) => { + const newSetting = filteredSettings.find(s => s._id === setting.id); + return setting.prepareUpdate(protectedFunction((s) => { + Object.assign(s, newSetting); + })); }); + + const allRecords = [ + ...settingsToCreate, + ...settingsToUpdate + ]; + + try { + await db.batch(...allRecords); + } catch (e) { + log(e); + } + return allRecords.length; }); } catch (e) { log(e); diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 06ff12eda2..cfa9599198 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -288,6 +288,8 @@ const RocketChat = { const serversDB = database.servers; reduxStore.dispatch(shareSelectServer(server)); + RocketChat.setCustomEmojis(); + // set User info try { const userId = await RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`); @@ -320,7 +322,7 @@ const RocketChat = { updateJitsiTimeout(roomId) { // RC 0.74.0 - return this.post('jitsi.updateTimeout', { roomId }); + return this.post('video-conference/jitsi.update-timeout', { roomId }); }, register(credentials) { @@ -1075,6 +1077,10 @@ const RocketChat = { return 'cas'; } + if (authName === 'apple' && isIOS) { + return 'apple'; + } + // TODO: remove this after other oauth providers are implemented. e.g. Drupal, github_enterprise const availableOAuth = ['facebook', 'github', 'gitlab', 'google', 'linkedin', 'meteor-developer', 'twitter', 'wordpress']; return availableOAuth.includes(authName) ? 'oauth' : 'not_supported'; diff --git a/app/presentation/ImageViewer/ImageViewer.android.js b/app/presentation/ImageViewer/ImageViewer.android.js index 9c3a290492..2cd8578bb1 100644 --- a/app/presentation/ImageViewer/ImageViewer.android.js +++ b/app/presentation/ImageViewer/ImageViewer.android.js @@ -260,6 +260,21 @@ function bouncy( const WIDTH = 300; const HEIGHT = 300; +class Image extends React.PureComponent { + static propTypes = { + imageComponentType: PropTypes.string + } + + render() { + const { imageComponentType } = this.props; + + const Component = ImageComponent(imageComponentType); + + return ; + } +} +const AnimatedImage = Animated.createAnimatedComponent(Image); + // it was picked from https://github.com/software-mansion/react-native-reanimated/tree/master/Example/imageViewer // and changed to use FastImage animated component export class ImageViewer extends React.Component { @@ -386,12 +401,9 @@ export class ImageViewer extends React.Component { render() { const { - uri, width, height, theme, imageComponentType, ...props + uri, width, height, imageComponentType, theme, ...props } = this.props; - const Component = ImageComponent(imageComponentType); - const AnimatedFastImage = Animated.createAnimatedComponent(Component); - // The below two animated values makes it so that scale appears to be done // from the top left corner of the image view instead of its center. This // is required for the "scale focal point" math to work correctly @@ -416,7 +428,7 @@ export class ImageViewer extends React.Component { onGestureEvent={this._onPanEvent} onHandlerStateChange={this._onPanEvent} > - { const translateX = multiply( current.progress.interpolate({ @@ -65,13 +64,12 @@ const forStackAndroid = ({ inverted ); - const opacity = conditional( - closing, - current.progress, + const opacity = multiply( current.progress.interpolate({ inputRange: [0, 1], outputRange: [0, 1] - }) + }), + inverted ); return { diff --git a/app/utils/navigation/index.js b/app/utils/navigation/index.js index e3492df605..6f4faf6c13 100644 --- a/app/utils/navigation/index.js +++ b/app/utils/navigation/index.js @@ -46,9 +46,9 @@ export const navigationTheme = (theme) => { // Gets the current screen from navigation state export const getActiveRoute = (state) => { - const route = state.routes[state.index]; + const route = state?.routes[state?.index]; - if (route.state) { + if (route?.state) { // Dive into nested navigators return getActiveRoute(route.state); } @@ -56,4 +56,4 @@ export const getActiveRoute = (state) => { return route; }; -export const getActiveRouteName = state => getActiveRoute(state).name; +export const getActiveRouteName = state => getActiveRoute(state)?.name; diff --git a/app/views/AttachmentView.js b/app/views/AttachmentView.js index a4b110c72a..6f43823da8 100644 --- a/app/views/AttachmentView.js +++ b/app/views/AttachmentView.js @@ -70,10 +70,15 @@ class AttachmentView extends React.Component { setHeader = () => { const { route, navigation, theme } = this.props; const attachment = route.params?.attachment; - const { title } = attachment; + let { title } = attachment; + try { + title = decodeURI(title); + } catch { + // Do nothing + } const options = { + title, headerLeft: () => , - title: decodeURI(title), headerRight: () => , headerBackground: () => , headerTintColor: themes[theme].previewTintColor, diff --git a/app/views/NotificationPreferencesView/index.js b/app/views/NotificationPreferencesView/index.js index 1c0f43c8cc..31a0fc679b 100644 --- a/app/views/NotificationPreferencesView/index.js +++ b/app/views/NotificationPreferencesView/index.js @@ -16,6 +16,7 @@ import RocketChat from '../../lib/rocketchat'; import { withTheme } from '../../theme'; import protectedFunction from '../../lib/methods/helpers/protectedFunction'; import SafeAreaView from '../../containers/SafeAreaView'; +import log from '../../utils/log'; const SectionTitle = React.memo(({ title, theme }) => ( { - await room.update(protectedFunction((r) => { - r[key] = value; - })); - }); - try { - const result = await RocketChat.saveNotificationSettings(this.rid, params); - if (result.success) { - return; + await db.action(async() => { + await room.update(protectedFunction((r) => { + r[key] = value; + })); + }); + + try { + const result = await RocketChat.saveNotificationSettings(this.rid, params); + if (result.success) { + return; + } + } catch { + // do nothing } - } catch { - // do nothing - } - await db.action(async() => { - await room.update(protectedFunction((r) => { - r[key] = room[key]; - })); - }); + await db.action(async() => { + await room.update(protectedFunction((r) => { + r[key] = room[key]; + })); + }); + } catch (e) { + log(e); + } } onValueChangeSwitch = (key, value) => this.saveNotificationSettings(key, value, { [key]: value ? '1' : '0' }); diff --git a/app/views/RegisterView.js b/app/views/RegisterView.js index 1fe1274164..1e0ce2654f 100644 --- a/app/views/RegisterView.js +++ b/app/views/RegisterView.js @@ -145,10 +145,12 @@ class RegisterView extends React.Component { await loginRequest({ user: email, password }); } } catch (e) { - if (e.data && e.data.errorType === 'username-invalid') { + if (e.data?.errorType === 'username-invalid') { return loginRequest({ user: email, password }); } - showErrorAlert(e.data.error, I18n.t('Oops')); + if (e.data?.error) { + showErrorAlert(e.data.error, I18n.t('Oops')); + } } this.setState({ saving: false }); } diff --git a/app/views/RoomView/Header/Header.js b/app/views/RoomView/Header/Header.js index 58792de2e9..9022514ab0 100644 --- a/app/views/RoomView/Header/Header.js +++ b/app/views/RoomView/Header/Header.js @@ -6,28 +6,20 @@ import { import I18n from '../../../i18n'; import sharedStyles from '../../Styles'; -import { isAndroid, isTablet } from '../../../utils/deviceInfo'; import Icon from './Icon'; import { themes } from '../../../constants/colors'; import Markdown from '../../../containers/markdown'; -const androidMarginLeft = isTablet ? 0 : 4; - const TITLE_SIZE = 16; const styles = StyleSheet.create({ container: { flex: 1, - marginRight: isAndroid ? 15 : 5, - marginLeft: isAndroid ? androidMarginLeft : -10, justifyContent: 'center' }, titleContainer: { alignItems: 'center', flexDirection: 'row' }, - threadContainer: { - marginRight: isAndroid ? 20 : undefined - }, title: { ...sharedStyles.textSemibold, fontSize: TITLE_SIZE @@ -36,7 +28,6 @@ const styles = StyleSheet.create({ alignItems: 'center' }, subtitle: { - marginRight: -16, ...sharedStyles.textRegular, fontSize: 12 }, @@ -87,12 +78,8 @@ SubTitle.propTypes = { }; const HeaderTitle = React.memo(({ - title, tmid, prid, scale, connecting, theme + title, tmid, prid, scale, theme }) => { - if (connecting) { - title = I18n.t('Connecting'); - } - if (!tmid && !prid) { return ( - + { if (!isMasterDetail || tmid) { const onPress = useCallback(() => navigation.goBack()); + const label = unreadsCount > 99 ? '+99' : unreadsCount || ' '; + const labelLength = label.length ? label.length : 1; + const marginLeft = -2 * labelLength; + const fontSize = labelLength > 1 ? 14 : 17; return ( 999 ? '+999' : unreadsCount || ' '} + label={label} onPress={onPress} tintColor={themes[theme].headerTintColor} + labelStyle={{ fontSize, marginLeft }} /> ); } @@ -44,7 +49,7 @@ const RoomHeaderLeft = React.memo(({ return null; }); -RoomHeaderLeft.propTypes = { +LeftButtons.propTypes = { tmid: PropTypes.string, unreadsCount: PropTypes.number, navigation: PropTypes.object, @@ -58,4 +63,4 @@ RoomHeaderLeft.propTypes = { isMasterDetail: PropTypes.bool }; -export default RoomHeaderLeft; +export default LeftButtons; diff --git a/app/views/RoomView/Header/RightButtons.js b/app/views/RoomView/Header/RightButtons.js index caec40d226..201866fe61 100644 --- a/app/views/RoomView/Header/RightButtons.js +++ b/app/views/RoomView/Header/RightButtons.js @@ -68,6 +68,17 @@ class RightButtonsContainer extends React.PureComponent { } } + goSearchView = () => { + const { + rid, navigation, isMasterDetail + } = this.props; + if (isMasterDetail) { + navigation.navigate('ModalStackNavigator', { screen: 'SearchMessagesView', params: { rid, showCloseModal: true } }); + } else { + navigation.navigate('SearchMessagesView', { rid }); + } + } + toggleFollowThread = () => { const { isFollowingThread } = this.state; const { toggleFollowThread } = this.props; @@ -104,6 +115,12 @@ class RightButtonsContainer extends React.PureComponent { testID='room-view-header-threads' /> ) : null} + ); } diff --git a/app/views/RoomView/Header/index.js b/app/views/RoomView/Header/index.js index e94b58ca2f..61b4eb1cdc 100644 --- a/app/views/RoomView/Header/index.js +++ b/app/views/RoomView/Header/index.js @@ -4,10 +4,11 @@ import { connect } from 'react-redux'; import equal from 'deep-equal'; import Header from './Header'; +import LeftButtons from './LeftButtons'; import RightButtons from './RightButtons'; import { withTheme } from '../../../theme'; -import RoomHeaderLeft from './RoomHeaderLeft'; import { withDimensions } from '../../../dimensions'; +import I18n from '../../../i18n'; class RoomHeaderView extends Component { static propTypes = { @@ -20,6 +21,7 @@ class RoomHeaderView extends Component { status: PropTypes.string, statusText: PropTypes.string, connecting: PropTypes.bool, + connected: PropTypes.bool, theme: PropTypes.string, roomUserId: PropTypes.string, widthOffset: PropTypes.number, @@ -30,7 +32,7 @@ class RoomHeaderView extends Component { shouldComponentUpdate(nextProps) { const { - type, title, subtitle, status, statusText, connecting, goRoomActionsView, usersTyping, theme, width, height + type, title, subtitle, status, statusText, connecting, connected, goRoomActionsView, usersTyping, theme, width, height } = this.props; if (nextProps.theme !== theme) { return true; @@ -53,6 +55,9 @@ class RoomHeaderView extends Component { if (nextProps.connecting !== connecting) { return true; } + if (nextProps.connected !== connected) { + return true; + } if (nextProps.width !== width) { return true; } @@ -70,9 +75,18 @@ class RoomHeaderView extends Component { render() { const { - title, subtitle, type, prid, tmid, widthOffset, status = 'offline', statusText, connecting, usersTyping, goRoomActionsView, roomUserId, theme, width, height + title, subtitle: subtitleProp, type, prid, tmid, widthOffset, status = 'offline', statusText, connecting, connected, usersTyping, goRoomActionsView, roomUserId, theme, width, height } = this.props; + let subtitle; + if (connecting) { + subtitle = I18n.t('Connecting'); + } else if (!connected) { + subtitle = I18n.t('Waiting_for_network'); + } else { + subtitle = subtitleProp; + } + return (
{ } return { - connecting: state.meteor.connecting, + connecting: state.meteor.connecting || state.server.loading, + connected: state.meteor.connected, usersTyping: state.usersTyping, status, statusText @@ -117,4 +132,4 @@ const mapStateToProps = (state, ownProps) => { export default connect(mapStateToProps)(withDimensions(withTheme(RoomHeaderView))); -export { RightButtons, RoomHeaderLeft }; +export { RightButtons, LeftButtons }; diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index 62553d3685..0b6afd18f0 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -8,6 +8,7 @@ import moment from 'moment'; import * as Haptics from 'expo-haptics'; import { Q } from '@nozbe/watermelondb'; import isEqual from 'lodash/isEqual'; +import { withSafeAreaInsets } from 'react-native-safe-area-context'; import Touch from '../../utils/touch'; import { @@ -26,7 +27,7 @@ import styles from './styles'; import log from '../../utils/log'; import EventEmitter from '../../utils/events'; import I18n from '../../i18n'; -import RoomHeaderView, { RightButtons, RoomHeaderLeft } from './Header'; +import RoomHeaderView, { RightButtons, LeftButtons } from './Header'; import StatusBar from '../../containers/StatusBar'; import Separator from './Separator'; import { themes } from '../../constants/colors'; @@ -53,6 +54,7 @@ import Banner from './Banner'; import Navigation from '../../lib/Navigation'; import SafeAreaView from '../../containers/SafeAreaView'; import { withDimensions } from '../../dimensions'; +import { getHeaderTitlePosition } from '../../containers/Header'; const stateAttrsUpdate = [ 'joined', @@ -91,7 +93,8 @@ class RoomView extends React.Component { theme: PropTypes.string, replyBroadcast: PropTypes.func, width: PropTypes.number, - height: PropTypes.number + height: PropTypes.number, + insets: PropTypes.object }; constructor(props) { @@ -178,7 +181,7 @@ class RoomView extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { state } = this; const { roomUpdate, member } = state; - const { appState, theme } = this.props; + const { appState, theme, insets } = this.props; if (theme !== nextProps.theme) { return true; } @@ -192,12 +195,15 @@ class RoomView extends React.Component { if (stateUpdated) { return true; } + if (!isEqual(nextProps.insets, insets)) { + return true; + } return roomAttrsUpdate.some(key => !isEqual(nextState.roomUpdate[key], roomUpdate[key])); } componentDidUpdate(prevProps, prevState) { const { roomUpdate } = this.state; - const { appState } = this.props; + const { appState, insets } = this.props; if (appState === 'foreground' && appState !== prevProps.appState && this.rid) { this.onForegroundInteraction = InteractionManager.runAfterInteractions(() => { @@ -222,6 +228,9 @@ class RoomView extends React.Component { if (((roomUpdate.fname !== prevState.roomUpdate.fname) || (roomUpdate.name !== prevState.roomUpdate.name)) && !this.tmid) { this.setHeader(); } + if (insets.left !== prevProps.insets.left || insets.right !== prevProps.insets.right) { + this.setHeader(); + } this.setReadOnly(); } @@ -281,7 +290,7 @@ class RoomView extends React.Component { setHeader = () => { const { room, unreadsCount, roomUserId: stateRoomUserId } = this.state; const { - navigation, route, isMasterDetail, theme, baseUrl, user + navigation, route, isMasterDetail, theme, baseUrl, user, insets } = this.props; const rid = route.params?.rid; const prid = route.params?.prid; @@ -299,9 +308,29 @@ class RoomView extends React.Component { if (!rid) { return; } + const headerTitlePosition = getHeaderTitlePosition(insets); navigation.setOptions({ headerShown: true, headerTitleAlign: 'left', + headerTitleContainerStyle: { + left: headerTitlePosition.left, + right: headerTitlePosition.right + }, + headerLeft: () => ( + + ), headerTitle: () => ( - ), - headerLeft: () => ( - ) }); } @@ -755,7 +769,7 @@ class RoomView extends React.Component { if (handleCommandScroll(event)) { const offset = input === 'UIKeyInputUpArrow' ? 100 : -100; this.offset += offset; - this.flatList.scrollToOffset({ offset: this.offset }); + this.flatList?.scrollToOffset({ offset: this.offset }); } else if (handleCommandRoomActions(event)) { this.goRoomActionsView(); } else if (handleCommandSearchMessages(event)) { @@ -1040,4 +1054,4 @@ const mapDispatchToProps = dispatch => ({ replyBroadcast: message => dispatch(replyBroadcastAction(message)) }); -export default connect(mapStateToProps, mapDispatchToProps)(withDimensions(withTheme(RoomView))); +export default connect(mapStateToProps, mapDispatchToProps)(withDimensions(withTheme(withSafeAreaInsets(RoomView)))); diff --git a/app/views/RoomsListView/Header/Header.ios.js b/app/views/RoomsListView/Header/Header.ios.js deleted file mode 100644 index e45d8baee8..0000000000 --- a/app/views/RoomsListView/Header/Header.ios.js +++ /dev/null @@ -1,95 +0,0 @@ -import React from 'react'; -import { - Text, View, TouchableOpacity, StyleSheet -} from 'react-native'; -import PropTypes from 'prop-types'; - -import I18n from '../../../i18n'; -import sharedStyles from '../../Styles'; -import { themes } from '../../../constants/colors'; -import { CustomIcon } from '../../../lib/Icons'; - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center' - }, - button: { - flexDirection: 'row', - alignItems: 'center' - }, - title: { - fontSize: 14, - ...sharedStyles.textRegular - }, - server: { - fontSize: 12, - ...sharedStyles.textRegular - }, - disclosure: { - marginLeft: 3, - marginTop: 1, - width: 12, - height: 9 - }, - upsideDown: { - transform: [{ scaleY: -1 }] - } -}); - -const HeaderTitle = React.memo(({ connecting, isFetching, theme }) => { - let title = I18n.t('Messages'); - if (connecting) { - title = I18n.t('Connecting'); - } - if (isFetching) { - title = I18n.t('Updating'); - } - return {title}; -}); - -const Header = React.memo(({ - connecting, isFetching, serverName, showServerDropdown, onPress, theme -}) => ( - - - - - {serverName} - - - - -)); - -Header.propTypes = { - connecting: PropTypes.bool, - isFetching: PropTypes.bool, - serverName: PropTypes.string, - theme: PropTypes.string, - showServerDropdown: PropTypes.bool.isRequired, - onPress: PropTypes.func.isRequired -}; - -Header.defaultProps = { - serverName: 'Rocket.Chat' -}; - -HeaderTitle.propTypes = { - connecting: PropTypes.bool, - isFetching: PropTypes.bool, - theme: PropTypes.string -}; - -export default Header; diff --git a/app/views/RoomsListView/Header/Header.android.js b/app/views/RoomsListView/Header/Header.js similarity index 54% rename from app/views/RoomsListView/Header/Header.android.js rename to app/views/RoomsListView/Header/Header.js index d1d4fb745d..839048bc06 100644 --- a/app/views/RoomsListView/Header/Header.android.js +++ b/app/views/RoomsListView/Header/Header.js @@ -9,26 +9,23 @@ import I18n from '../../../i18n'; import sharedStyles from '../../Styles'; import { themes } from '../../../constants/colors'; import { CustomIcon } from '../../../lib/Icons'; +import { isTablet, isIOS } from '../../../utils/deviceInfo'; +import { useOrientation } from '../../../dimensions'; const styles = StyleSheet.create({ container: { flex: 1, - justifyContent: 'center' + justifyContent: 'center', + marginLeft: isTablet ? 10 : 0 }, button: { flexDirection: 'row', - alignItems: 'center', - marginRight: 64 + alignItems: 'center' }, - server: { - fontSize: 20, - ...sharedStyles.textRegular - }, - serverSmall: { - fontSize: 16 + title: { + ...sharedStyles.textSemibold }, - updating: { - fontSize: 14, + subtitle: { ...sharedStyles.textRegular }, upsideDown: { @@ -37,41 +34,55 @@ const styles = StyleSheet.create({ }); const Header = React.memo(({ - connecting, isFetching, serverName, showServerDropdown, showSearchHeader, theme, onSearchChangeText, onPress + connecting, connected, isFetching, serverName, server, showServerDropdown, showSearchHeader, theme, onSearchChangeText, onPress }) => { const titleColorStyle = { color: themes[theme].headerTitleColor }; const isLight = theme === 'light'; + const { isLandscape } = useOrientation(); + const scale = isIOS && isLandscape && !isTablet ? 0.8 : 1; + const titleFontSize = 16 * scale; + const subTitleFontSize = 12 * scale; + if (showSearchHeader) { return ( ); } + let subtitle; + if (connecting) { + subtitle = I18n.t('Connecting'); + } else if (isFetching) { + subtitle = I18n.t('Updating'); + } else if (!connected) { + subtitle = I18n.t('Waiting_for_network'); + } else { + subtitle = server?.replace(/(^\w+:|^)\/\//, ''); + } return ( - {connecting ? {I18n.t('Connecting')} : null} - {isFetching ? {I18n.t('Updating')} : null} - {serverName} + {serverName} + {subtitle ? {subtitle} : null} ); @@ -83,8 +94,10 @@ Header.propTypes = { onPress: PropTypes.func.isRequired, onSearchChangeText: PropTypes.func.isRequired, connecting: PropTypes.bool, + connected: PropTypes.bool, isFetching: PropTypes.bool, serverName: PropTypes.string, + server: PropTypes.string, theme: PropTypes.string }; diff --git a/app/views/RoomsListView/Header/index.js b/app/views/RoomsListView/Header/index.js index badcb59acf..12a57dc00f 100644 --- a/app/views/RoomsListView/Header/index.js +++ b/app/views/RoomsListView/Header/index.js @@ -18,8 +18,10 @@ class RoomsListHeaderView extends PureComponent { showSearchHeader: PropTypes.bool, serverName: PropTypes.string, connecting: PropTypes.bool, + connected: PropTypes.bool, isFetching: PropTypes.bool, theme: PropTypes.string, + server: PropTypes.string, open: PropTypes.func, close: PropTypes.func, closeSort: PropTypes.func, @@ -68,16 +70,18 @@ class RoomsListHeaderView extends PureComponent { render() { const { - serverName, showServerDropdown, showSearchHeader, connecting, isFetching, theme + serverName, showServerDropdown, showSearchHeader, connecting, connected, isFetching, theme, server } = this.props; return (
({ showSortDropdown: state.rooms.showSortDropdown, showSearchHeader: state.rooms.showSearchHeader, connecting: state.meteor.connecting || state.server.loading, + connected: state.meteor.connected, isFetching: state.rooms.isFetching, - serverName: state.settings.Site_Name + serverName: state.settings.Site_Name, + server: state.server.server }); const mapDispatchtoProps = dispatch => ({ diff --git a/app/views/RoomsListView/ListHeader/SearchBar.js b/app/views/RoomsListView/ListHeader/SearchBar.js deleted file mode 100644 index 6446d4cf4d..0000000000 --- a/app/views/RoomsListView/ListHeader/SearchBar.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import SearchBox from '../../../containers/SearchBox'; -import { isIOS } from '../../../utils/deviceInfo'; -import { withTheme } from '../../../theme'; - -const SearchBar = React.memo(({ - theme, onChangeSearchText, inputRef, searching, onCancelSearchPress, onSearchFocus -}) => { - if (isIOS) { - return ( - - ); - } - return null; -}); - -SearchBar.propTypes = { - theme: PropTypes.string, - searching: PropTypes.bool, - inputRef: PropTypes.func, - onChangeSearchText: PropTypes.func, - onCancelSearchPress: PropTypes.func, - onSearchFocus: PropTypes.func -}; - -export default withTheme(SearchBar); diff --git a/app/views/RoomsListView/ListHeader/index.js b/app/views/RoomsListView/ListHeader/index.js index 63d20ca9e5..dec38b5063 100644 --- a/app/views/RoomsListView/ListHeader/index.js +++ b/app/views/RoomsListView/ListHeader/index.js @@ -1,28 +1,16 @@ import React from 'react'; import PropTypes from 'prop-types'; -import SearchBar from './SearchBar'; import Directory from './Directory'; import Sort from './Sort'; const ListHeader = React.memo(({ searching, sortBy, - onChangeSearchText, toggleSort, - goDirectory, - inputRef, - onCancelSearchPress, - onSearchFocus + goDirectory }) => ( <> - @@ -31,12 +19,8 @@ const ListHeader = React.memo(({ ListHeader.propTypes = { searching: PropTypes.bool, sortBy: PropTypes.string, - onChangeSearchText: PropTypes.func, toggleSort: PropTypes.func, - goDirectory: PropTypes.func, - inputRef: PropTypes.func, - onCancelSearchPress: PropTypes.func, - onSearchFocus: PropTypes.func + goDirectory: PropTypes.func }; export default ListHeader; diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index 39b035e6f2..7543a46f4f 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -12,6 +12,7 @@ import { connect } from 'react-redux'; import { isEqual, orderBy } from 'lodash'; import Orientation from 'react-native-orientation-locker'; import { Q } from '@nozbe/watermelondb'; +import { withSafeAreaInsets } from 'react-native-safe-area-context'; import database from '../../lib/database'; import RocketChat from '../../lib/rocketchat'; @@ -30,7 +31,7 @@ import { } from '../../actions/rooms'; import { appStart as appStartAction, ROOT_BACKGROUND } from '../../actions/app'; import debounce from '../../utils/debounce'; -import { isIOS, isAndroid, isTablet } from '../../utils/deviceInfo'; +import { isIOS, isTablet } from '../../utils/deviceInfo'; import RoomsListHeaderView from './Header'; import { DrawerButton, @@ -59,10 +60,9 @@ import { MAX_SIDEBAR_WIDTH } from '../../constants/tablet'; import { getUserSelector } from '../../selectors/login'; import { goRoom } from '../../utils/goRoom'; import SafeAreaView from '../../containers/SafeAreaView'; -import Header from '../../containers/Header'; +import Header, { getHeaderTitlePosition } from '../../containers/Header'; import { withDimensions } from '../../dimensions'; -const SCROLL_OFFSET = 56; const INITIAL_NUM_TO_RENDER = isTablet ? 20 : 12; const CHATS_HEADER = 'Chats'; const UNREAD_HEADER = 'Unread'; @@ -129,7 +129,8 @@ class RoomsListView extends React.Component { connected: PropTypes.bool, isMasterDetail: PropTypes.bool, rooms: PropTypes.array, - width: PropTypes.number + width: PropTypes.number, + insets: PropTypes.object }; constructor(props) { @@ -242,7 +243,7 @@ class RoomsListView extends React.Component { loading, search } = this.state; - const { rooms, width } = this.props; + const { rooms, width, insets } = this.props; if (nextState.loading !== loading) { return true; } @@ -255,6 +256,9 @@ class RoomsListView extends React.Component { if (!isEqual(nextProps.rooms, rooms)) { return true; } + if (!isEqual(nextProps.insets, insets)) { + return true; + } // If it's focused and there are changes, update if (chatsNotEqual) { this.shouldUpdate = false; @@ -273,7 +277,8 @@ class RoomsListView extends React.Component { connected, roomsRequest, rooms, - isMasterDetail + isMasterDetail, + insets } = this.props; const { item } = this.state; @@ -298,6 +303,9 @@ class RoomsListView extends React.Component { // eslint-disable-next-line react/no-did-update-set-state this.setState({ item: { rid: rooms[0] } }); } + if (insets.left !== prevProps.insets.left || insets.right !== prevProps.insets.right) { + this.setHeader(); + } } componentWillUnmount() { @@ -318,9 +326,11 @@ class RoomsListView extends React.Component { getHeader = () => { const { searching } = this.state; - const { navigation, isMasterDetail } = this.props; + const { navigation, isMasterDetail, insets } = this.props; + const headerTitlePosition = getHeaderTitlePosition(insets); return { - headerLeft: () => (searching && isAndroid ? ( + headerTitleAlign: 'left', + headerLeft: () => (searching ? ( navigation.navigate('ModalStackNavigator', { screen: 'SettingsView' }) : () => navigation.toggleDrawer()} + onPress={isMasterDetail + ? () => navigation.navigate('ModalStackNavigator', { screen: 'SettingsView' }) + : () => navigation.toggleDrawer()} /> )), headerTitle: () => , - headerRight: () => (searching && isAndroid ? null : ( + headerTitleContainerStyle: { + left: headerTitlePosition.left, + right: headerTitlePosition.right + }, + headerRight: () => (searching ? null : ( - {isAndroid ? ( - - ) : null} navigation.navigate('ModalStackNavigator', { screen: 'NewMessageView' }) : () => navigation.navigate('NewMessageStackNavigator')} + onPress={isMasterDetail + ? () => navigation.navigate('ModalStackNavigator', { screen: 'NewMessageView' }) + : () => navigation.navigate('NewMessageStackNavigator')} testID='rooms-list-view-create-channel' /> + )) }; @@ -411,7 +428,7 @@ class RoomsListView extends React.Component { let tempChats = []; let chats = []; if (sortBy === 'alphabetical') { - chats = orderBy(data, ['name'], ['asc']); + chats = orderBy(data, [`${ this.useRealName ? 'fname' : 'name' }`], ['asc']); } else { chats = orderBy(data, ['roomUpdatedAt'], ['desc']); } @@ -476,10 +493,8 @@ class RoomsListView extends React.Component { initSearching = () => { const { openSearchHeader } = this.props; this.internalSetState({ searching: true }, () => { - if (isAndroid) { - openSearchHeader(); - this.setHeader(); - } + openSearchHeader(); + this.setHeader(); }); }; @@ -493,23 +508,11 @@ class RoomsListView extends React.Component { Keyboard.dismiss(); - if (isIOS && this.inputRef) { - this.inputRef.blur(); - this.inputRef.clear(); - } - this.setState({ searching: false, search: [] }, () => { - if (isAndroid) { - this.setHeader(); - closeSearchHeader(); - } + this.setHeader(); + closeSearchHeader(); setTimeout(() => { - const offset = isAndroid ? 0 : SCROLL_OFFSET; - if (this.scroll.scrollTo) { - this.scroll.scrollTo({ x: 0, y: offset, animated: true }); - } else if (this.scroll.scrollToOffset) { - this.scroll.scrollToOffset({ offset }); - } + this.scrollToTop(); }, 200); }); }; @@ -538,9 +541,7 @@ class RoomsListView extends React.Component { search: result, searching: true }); - if (this.scroll && this.scroll.scrollTo) { - this.scroll.scrollTo({ x: 0, y: 0, animated: true }); - } + this.scrollToTop(); }, 300); getRoomTitle = item => RocketChat.getRoomTitle(item) @@ -561,15 +562,16 @@ class RoomsListView extends React.Component { this.goRoom({ item, isMasterDetail }); }; + scrollToTop = () => { + if (this.scroll?.scrollToOffset) { + this.scroll.scrollToOffset({ offset: 0 }); + } + }; + toggleSort = () => { const { toggleSortDropdown } = this.props; - const offset = isAndroid ? 0 : SCROLL_OFFSET; - if (this.scroll.scrollTo) { - this.scroll.scrollTo({ x: 0, y: offset, animated: true }); - } else if (this.scroll.scrollToOffset) { - this.scroll.scrollToOffset({ offset }); - } + this.scrollToTop(); setTimeout(() => { toggleSortDropdown(); }, 100); @@ -714,8 +716,7 @@ class RoomsListView extends React.Component { if (handleCommandShowPreferences(event)) { navigation.navigate('SettingsView'); } else if (handleCommandSearching(event)) { - this.scroll.scrollToOffset({ animated: true, offset: 0 }); - this.inputRef.focus(); + this.initSearching(); } else if (handleCommandSelectRoom(event)) { this.goRoomByIndex(input); } else if (handleCommandPreviousRoom(event)) { @@ -744,19 +745,13 @@ class RoomsListView extends React.Component { getScrollRef = ref => (this.scroll = ref); - getInputRef = ref => (this.inputRef = ref); - renderListHeader = () => { const { searching } = this.state; const { sortBy } = this.props; return ( @@ -869,7 +864,6 @@ class RoomsListView extends React.Component { ref={this.getScrollRef} data={searching ? search : chats} extraData={searching ? search : chats} - contentOffset={isIOS ? { x: 0, y: SCROLL_OFFSET } : {}} keyExtractor={keyExtractor} style={[styles.list, { backgroundColor: themes[theme].backgroundColor }]} renderItem={this.renderItem} @@ -953,4 +947,4 @@ const mapDispatchToProps = dispatch => ({ closeServerDropdown: () => dispatch(closeServerDropdownAction()) }); -export default connect(mapStateToProps, mapDispatchToProps)(withDimensions(withTheme(RoomsListView))); +export default connect(mapStateToProps, mapDispatchToProps)(withDimensions(withTheme(withSafeAreaInsets(RoomsListView)))); diff --git a/app/views/RoomsListView/styles.js b/app/views/RoomsListView/styles.js index 9deb03dc12..70f2bb6dce 100644 --- a/app/views/RoomsListView/styles.js +++ b/app/views/RoomsListView/styles.js @@ -23,7 +23,7 @@ export default StyleSheet.create({ sortToggleText: { fontSize: 16, flex: 1, - marginLeft: 15, + marginLeft: 12, ...sharedStyles.textRegular }, dropdownContainer: { @@ -50,16 +50,16 @@ export default StyleSheet.create({ }, sortSeparator: { height: StyleSheet.hairlineWidth, - marginHorizontal: 15, + marginHorizontal: 12, flex: 1 }, sortIcon: { width: 22, height: 22, - marginHorizontal: 15 + marginHorizontal: 12 }, groupTitleContainer: { - paddingHorizontal: 15, + paddingHorizontal: 12, paddingTop: 17, paddingBottom: 10 }, @@ -75,12 +75,12 @@ export default StyleSheet.create({ }, serverHeaderText: { fontSize: 16, - marginLeft: 15, + marginLeft: 12, ...sharedStyles.textRegular }, serverHeaderAdd: { fontSize: 16, - marginRight: 15, + marginRight: 12, paddingVertical: 10, ...sharedStyles.textRegular }, @@ -95,7 +95,7 @@ export default StyleSheet.create({ serverIcon: { width: 42, height: 42, - marginHorizontal: 15, + marginHorizontal: 12, marginVertical: 13, borderRadius: 4, resizeMode: 'contain' @@ -120,7 +120,7 @@ export default StyleSheet.create({ directoryIcon: { width: 22, height: 22, - marginHorizontal: 15 + marginHorizontal: 12 }, directoryText: { fontSize: 16, diff --git a/app/views/ShareView/Preview.js b/app/views/ShareView/Preview.js index fae6795354..2aaff22d84 100644 --- a/app/views/ShareView/Preview.js +++ b/app/views/ShareView/Preview.js @@ -10,12 +10,13 @@ import { ImageViewer, types } from '../../presentation/ImageViewer'; import { themes } from '../../constants/colors'; import { useDimensions, useOrientation } from '../../dimensions'; import { getHeaderHeight } from '../../containers/Header'; -import { isIOS } from '../../utils/deviceInfo'; import { THUMBS_HEIGHT } from './constants'; import sharedStyles from '../Styles'; import { allowPreview } from './utils'; import I18n from '../../i18n'; +const MESSAGEBOX_HEIGHT = 56; + const styles = StyleSheet.create({ fileContainer: { alignItems: 'center', @@ -58,23 +59,24 @@ const Preview = React.memo(({ const { isLandscape } = useOrientation(); const insets = useSafeAreaInsets(); const headerHeight = getHeaderHeight(isLandscape); - const messageboxHeight = isIOS ? 56 : 0; const thumbsHeight = (length > 1) ? THUMBS_HEIGHT : 0; - const calculatedHeight = height - insets.top - insets.bottom - messageboxHeight - thumbsHeight - headerHeight; + const calculatedHeight = height - insets.top - insets.bottom - MESSAGEBOX_HEIGHT - thumbsHeight - headerHeight; if (item?.canUpload) { if (type?.match(/video/)) { return ( -