Compare commits

...

17 Commits

Author SHA1 Message Date
Mattermost Build
15e081f572 Bump app build number to 341 (#5112) (#5114)
(cherry picked from commit 3338511355)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2021-01-11 16:09:06 -07:00
Mattermost Build
1be535cc22 [MM-30976] fix viewing/joining archived channels using channel links (#5106) (#5113)
(cherry picked from commit aaba9fa472)

Co-authored-by: Ashish Bhate <bhate.ashish@gmail.com>
2021-01-11 15:59:14 -07:00
Mattermost Build
6a2f02be62 MM-31705 allow file local path to use multiple dots (#5109) (#5110)
* MM-31705 allow file local path to use multiple dots

* Add unit test

(cherry picked from commit 2e790212f9)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-01-11 17:04:49 -03:00
Mattermost Build
4f28e61632 MM-31873 fix race condition that prevented the device id to be registered on Android (#5104) (#5108)
(cherry picked from commit 0d590c742f)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-01-11 15:39:51 -03:00
Mattermost Build
c7cf32ebb8 [MM-31778]: fix false error message on channel join (#5098) (#5099)
(cherry picked from commit c3f86d1797)

Co-authored-by: Ashish Bhate <bhate.ashish@gmail.com>
2021-01-08 11:06:42 -07:00
Mattermost Build
142a04fac5 Bump app build number to 340 (#5096) (#5097)
(cherry picked from commit 407333b446)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2021-01-07 15:37:36 -07:00
Mattermost Build
60b82ea5a8 Default to an empty channel object (#5094) (#5095)
(cherry picked from commit ac85110ce7)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2021-01-07 15:18:29 -07:00
Mattermost Build
64989a728c Revert "[MM-29225] Define LSApplicationQueriesSchemes so Linking.canO… (#5067) (#5091)
* Revert "[MM-29225] Define LSApplicationQueriesSchemes so Linking.canOpenURL can work (#5007)"

This reverts commit f3baaa6aa3.

* Create and use tryOpenURL

* Add missing onError functions

* Update app/components/markdown/markdown_link/markdown_link.js

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
(cherry picked from commit 7702c050bf)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2021-01-06 09:21:17 -07:00
Weblate (bot)
7317ffeb21 Translations update from Weblate (#5088)
* Translated using Weblate (Korean)

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ko/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/pt_BR/

* Translated using Weblate (Romanian)

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ro/

* Translated using Weblate (Ukrainian)

Currently translated at 97.9% (632 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/uk/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/zh_Hans/

* Translated using Weblate (Japanese)

Currently translated at 100.0% (645 of 645 strings)

Translation: mattermost-languages-shipped/mattermost-mobile_master
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ja/

Co-authored-by: Lee Dae-yeop <leedaeyeop@gmail.com>
Co-authored-by: rodrigocorsi <rodrigocorsi@gmail.com>
Co-authored-by: Viorel-Cosmin Miron <cosmin@uhlhost.net>
Co-authored-by: Ivan Novikov <monah1744@gmail.com>
Co-authored-by: aeomin <lin@aeomin.net>
Co-authored-by: kaakaa <stooner.hoe@gmail.com>
2021-01-05 14:20:42 +01:00
Mattermost Build
49031c26d4 Bump app build number to 339 (#5086) (#5087)
(cherry picked from commit 53885d2bf9)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2021-01-04 15:53:57 -07:00
Mattermost Build
c133dab50f Bump app version number to 1.39.0 (#5084) (#5085)
(cherry picked from commit ff1901eb61)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2021-01-04 15:44:51 -07:00
Mattermost Build
47c0ff2655 Handle go to location from CommandResponse (#4620) (#5082)
* First draft to handle go to location on mobile

* Fix lint

* Fix test

* Remove unnecessary change

* Add not handled cases

* Add i18n missing string

* Fix typo

* Extract handleGotoLocation into an action

* Fix minor issues and extract showPermalinkView to an action

* Fix minor issues and extract showPermalinkView to an action

* Add missing change

* Fix this reference

* Remove unneeded event handlers, sort imports, early handle errors, make group channel visible, remove duplications and move functions to the right place

* Fix tests

* Handle error when opening permalink

(cherry picked from commit 7bb777f4b3)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
2021-01-04 15:17:51 -07:00
Mattermost Build
55fc50d7c2 Bump app build number to 338 and version to 1.38.1 (#5058) (#5060)
* Bump app build number to 338

* Bump app version number to 1.38.1

* Update fastlane

(cherry picked from commit 367534df12)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-12-18 21:06:33 -03:00
Mattermost Build
8b0c831814 Set Tablet orientation explicitly to all (#5049) (#5054)
(cherry picked from commit 673f10770d)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-12-18 20:42:16 -03:00
Mattermost Build
971d5990e8 Fix ChannelLoader prop warning (#5055) (#5057)
* Fix ChannelLoader prop warning

* Missing semicolon

(cherry picked from commit f577685264)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-12-18 13:57:55 -07:00
Mattermost Build
e91af36780 Update Rudder (#5048) (#5052)
(cherry picked from commit be75a688de)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2020-12-18 16:47:49 -03:00
Mattermost Build
70e5cace11 [MM-31376] Do not subtract offset from accessories container (#5042) (#5045)
* Do not subtract offset from accessories container

* Missing space

* Adjust autcomplete offsetY

* Adjust placement of autocomplete

* Space fix

* Unused onLayout

(cherry picked from commit 8fb6510a32)

Co-authored-by: Miguel Alatzar <migbot@users.noreply.github.com>
2020-12-17 19:25:06 -07:00
59 changed files with 728 additions and 244 deletions

View File

@@ -132,8 +132,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
versionCode 337
versionName "1.38.0"
versionCode 341
versionName "1.39.0"
multiDexEnabled = true
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'

View File

@@ -15,7 +15,7 @@ import Store from '@store/store';
Navigation.setDefaultOptions({
layout: {
orientation: [DeviceTypes.IS_TABLET ? undefined : 'portrait'],
orientation: [DeviceTypes.IS_TABLET ? 'all' : 'portrait'],
},
});

View File

@@ -213,6 +213,8 @@ export function handleSelectChannel(channelId) {
console.log('channel switch to', channel?.display_name, channelId, (Date.now() - dt), 'ms'); //eslint-disable-line
}
return {data: true};
};
}
@@ -222,7 +224,7 @@ export function handleSelectChannelByName(channelName, teamName, errorHandler) {
const {teams: currentTeams, currentTeamId} = state.entities.teams;
const currentTeam = currentTeams[currentTeamId];
const currentTeamName = currentTeam?.name;
const response = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName));
const response = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName, true));
const {error, data: channel} = response;
const currentChannelId = getCurrentChannelId(state);
@@ -258,7 +260,7 @@ export function handleSelectChannelByName(channelName, teamName, errorHandler) {
dispatch(handleSelectChannel(channel.id));
}
return null;
return {data: true};
};
}

View File

@@ -150,6 +150,7 @@ describe('Actions.Views.Channel', () => {
channelSelectors.getMyChannelMember = jest.fn(() => ({data: {member: {}}}));
const appChannelSelectors = require('app/selectors/channel');
const getChannelReachableOriginal = appChannelSelectors.getChannelReachable;
appChannelSelectors.getChannelReachable = jest.fn(() => true);
test('handleSelectChannelByName success', async () => {
@@ -232,6 +233,60 @@ describe('Actions.Views.Channel', () => {
expect(joinedChannel).toBe(true);
});
test('handleSelectChannelByName select archived channel with ExperimentalViewArchivedChannels enabled', async () => {
const archivedChannelStoreObj = {...storeObj};
archivedChannelStoreObj.entities.general.config.ExperimentalViewArchivedChannels = 'true';
store = mockStore(archivedChannelStoreObj);
appChannelSelectors.getChannelReachable = getChannelReachableOriginal;
actions.getChannelByNameAndTeamName = jest.fn(() => {
return {
type: MOCK_RECEIVE_CHANNEL_TYPE,
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL, delete_at: 100},
};
});
channelSelectors.getChannelByName = jest.fn(() => {
return {
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL, delete_at: 100},
};
});
const errorHandler = jest.fn();
await store.dispatch(handleSelectChannelByName('channel-id-3', currentTeamName, errorHandler));
const storeActions = store.getActions();
const receivedChannel = storeActions.some((action) => action.type === MOCK_RECEIVE_CHANNEL_TYPE);
expect(receivedChannel).toBe(true);
expect(errorHandler).not.toBeCalled();
});
test('handleSelectChannelByName select archived channel with ExperimentalViewArchivedChannels disabled', async () => {
const noArchivedChannelStoreObj = {...storeObj};
noArchivedChannelStoreObj.entities.general.config.ExperimentalViewArchivedChannels = 'false';
store = mockStore(noArchivedChannelStoreObj);
appChannelSelectors.getChannelReachable = getChannelReachableOriginal;
actions.getChannelByNameAndTeamName = jest.fn(() => {
return {
type: MOCK_RECEIVE_CHANNEL_TYPE,
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL, delete_at: 100},
};
});
channelSelectors.getChannelByName = jest.fn(() => {
return {
data: {id: 'channel-id-3', name: 'channel-id-3', display_name: 'Test Channel', type: General.OPEN_CHANNEL, delete_at: 100},
};
});
const errorHandler = jest.fn();
await store.dispatch(handleSelectChannelByName('channel-id-3', currentTeamName, errorHandler));
const storeActions = store.getActions();
const receivedChannel = storeActions.some((action) => action.type === MOCK_RECEIVE_CHANNEL_TYPE);
expect(receivedChannel).toBe(true);
expect(errorHandler).toBeCalled();
});
test('loadPostsIfNecessaryWithRetry for the first time', async () => {
store = mockStore(storeObj);

View File

@@ -40,6 +40,7 @@ export function showPermalink(intl: typeof intlShape, teamName: string, postId:
showModalOverCurrentContext(screen, passProps, options);
}
}
return {};
};
}

View File

@@ -17,6 +17,4 @@ function mapStateToProps(state) {
};
}
export const AUTOCOMPLETE_MAX_HEIGHT = 200;
export default connect(mapStateToProps, null, null, {forwardRef: true})(Autocomplete);

View File

@@ -42,7 +42,7 @@ export default class ChannelLoader extends PureComponent {
style: CustomPropTypes.Style,
theme: PropTypes.object.isRequired,
height: PropTypes.number,
retryLoad: PropTypes.func.isRequired,
retryLoad: PropTypes.func,
};
constructor(props) {
@@ -75,8 +75,10 @@ export default class ChannelLoader extends PureComponent {
}
componentDidMount() {
this.stillLoadingTimeout = setTimeout(this.showIndicator, 10000);
this.retryLoadInterval = setInterval(this.props.retryLoad, 10000);
if (this.props.retryLoad) {
this.stillLoadingTimeout = setTimeout(this.showIndicator, 10000);
this.retryLoadInterval = setInterval(this.props.retryLoad, 10000);
}
}
componentWillUnmount() {

View File

@@ -14,7 +14,6 @@ describe('ChannelLoader', () => {
const baseProps = {
channelIsLoading: true,
theme: Preferences.THEMES.default,
retryLoad: jest.fn(),
};
test('should match snapshot', () => {
@@ -23,15 +22,26 @@ describe('ChannelLoader', () => {
});
test('should call setTimeout and setInterval for showIndicator and retryLoad on mount', () => {
const wrapper = shallow(<ChannelLoader {...baseProps}/>);
const instance = wrapper.instance();
shallow(<ChannelLoader {...baseProps}/>);
expect(setTimeout).not.toHaveBeenCalled();
expect(setInterval).not.toHaveBeenCalled();
const props = {
...baseProps,
retryLoad: jest.fn(),
};
const wrapper = shallow(<ChannelLoader {...props}/>);
const instance = wrapper.instance();
expect(setTimeout).toHaveBeenCalledWith(instance.showIndicator, 10000);
expect(setInterval).toHaveBeenCalledWith(baseProps.retryLoad, 10000);
expect(setInterval).toHaveBeenCalledWith(props.retryLoad, 10000);
});
test('should clear timer and interval on unmount', () => {
const wrapper = shallow(<ChannelLoader {...baseProps}/>);
const props = {
...baseProps,
retryLoad: jest.fn(),
};
const wrapper = shallow(<ChannelLoader {...props}/>);
const instance = wrapper.instance();
instance.componentWillUnmount();

View File

@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
import {
Alert,
Animated,
Linking,
TouchableOpacity,
View,
} from 'react-native';
@@ -17,6 +16,7 @@ import FormattedText from '@components/formatted_text';
import {DeviceTypes} from '@constants';
import {checkUpgradeType, isUpgradeAvailable} from '@utils/client_upgrade';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
import {showModal, dismissModal} from '@actions/navigation';
const {View: AnimatedView} = Animated;
@@ -121,11 +121,7 @@ export default class ClientUpgradeListener extends PureComponent {
const {downloadLink} = this.props;
const {intl} = this.context;
Linking.canOpenURL(downloadLink).then((supported) => {
if (supported) {
return Linking.openURL(downloadLink);
}
const onError = () => {
Alert.alert(
intl.formatMessage({
id: 'mobile.client_upgrade.download_error.title',
@@ -136,9 +132,8 @@ export default class ClientUpgradeListener extends PureComponent {
defaultMessage: 'An error occurred while trying to open the download link.',
}),
);
return false;
});
};
tryOpenURL(downloadLink, onError);
this.toggleUpgradeMessage(false);
};

View File

@@ -13,12 +13,13 @@ import {SafeAreaView} from 'react-native-safe-area-context';
import {General} from '@mm-redux/constants';
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
import Autocomplete from 'app/components/autocomplete';
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import StatusBar from 'app/components/status_bar';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import DEVICE from '@constants/device';
import {
changeOpacity,
@@ -373,7 +374,7 @@ export default class EditChannelInfo extends PureComponent {
<View style={[style.autocompleteContainer, bottomStyle]}>
<Autocomplete
cursorPosition={header.length}
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
maxHeight={DEVICE.AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.onHeaderChangeText}
value={header}
nestedScrollEnabled={true}

View File

@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import {intlShape} from 'react-intl';
import {
Linking,
Alert,
Platform,
StyleSheet,
Text,
@@ -24,7 +24,7 @@ import EphemeralStore from '@store/ephemeral_store';
import BottomSheet from '@utils/bottom_sheet';
import {generateId} from '@utils/file';
import {calculateDimensions, getViewPortWidth, isGifTooLarge, openGalleryAtIndex} from '@utils/images';
import {normalizeProtocol} from '@utils/url';
import {normalizeProtocol, tryOpenURL} from '@utils/url';
import mattermostManaged from 'app/mattermost_managed';
@@ -123,12 +123,22 @@ export default class MarkdownImage extends ImageViewPort {
handleLinkPress = () => {
const url = normalizeProtocol(this.props.linkDestination);
const {intl} = this.context;
Linking.canOpenURL(url).then((supported) => {
if (supported) {
Linking.openURL(url);
}
});
const onError = () => {
Alert.alert(
intl.formatMessage({
id: 'mobile.link.error.title',
defaultMessage: 'Error',
}),
intl.formatMessage({
id: 'mobile.link.error.text',
defaultMessage: 'Unable to open the link.',
}),
);
};
tryOpenURL(url, onError);
};
handleLinkLongPress = async () => {

View File

@@ -3,20 +3,19 @@
import React, {Children, PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Alert, Linking, Text} from 'react-native';
import {Alert, Text} from 'react-native';
import Clipboard from '@react-native-community/clipboard';
import urlParse from 'url-parse';
import {intlShape} from 'react-intl';
import urlParse from 'url-parse';
import Config from '@assets/config';
import {DeepLinkTypes} from '@constants';
import CustomPropTypes from '@constants/custom_prop_types';
import {getCurrentServerUrl} from '@init/credentials';
import BottomSheet from '@utils/bottom_sheet';
import {alertErrorWithFallback} from '@utils/general';
import {t} from '@utils/i18n';
import {errorBadChannel} from '@utils/draft';
import {preventDoubleTap} from '@utils/tap';
import {matchDeepLink, normalizeProtocol} from '@utils/url';
import {matchDeepLink, normalizeProtocol, tryOpenURL} from '@utils/url';
import mattermostManaged from 'app/mattermost_managed';
@@ -59,41 +58,30 @@ export default class MarkdownLink extends PureComponent {
if (match) {
if (match.type === DeepLinkTypes.CHANNEL) {
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, this.errorBadChannel);
const {intl} = this.context;
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, errorBadChannel.bind(null, intl));
} else if (match.type === DeepLinkTypes.PERMALINK) {
onPermalinkPress(match.postId, match.teamName);
}
} else {
Linking.canOpenURL(url).then((supported) => {
if (supported) {
Linking.openURL(url);
} else {
const {formatMessage} = this.context.intl;
Alert.alert(
formatMessage({
id: 'mobile.server_link.error.title',
defaultMessage: 'Link Error',
}),
formatMessage({
id: 'mobile.server_link.error.text',
defaultMessage: 'The link could not be found on this server.',
}),
);
}
});
const onError = () => {
const {formatMessage} = this.context.intl;
Alert.alert(
formatMessage({
id: 'mobile.link.error.title',
defaultMessage: 'Error',
}),
formatMessage({
id: 'mobile.link.error.text',
defaultMessage: 'Unable to open the link.',
}),
);
};
tryOpenURL(url, onError);
}
});
errorBadChannel = () => {
const {intl} = this.context;
const message = {
id: t('mobile.server_link.unreachable_channel.error'),
defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.',
};
alertErrorWithFallback(intl, {}, message);
};
parseLinkLiteral = (literal) => {
let nextLiteral = literal;

View File

@@ -2,11 +2,13 @@
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Linking, Text, View} from 'react-native';
import {Alert, Text, View} from 'react-native';
import FastImage from 'react-native-fast-image';
import {intlShape} from 'react-intl';
import PropTypes from 'prop-types';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
export default class AttachmentAuthor extends PureComponent {
static propTypes = {
@@ -16,10 +18,29 @@ export default class AttachmentAuthor extends PureComponent {
theme: PropTypes.object.isRequired,
};
static contextTypes = {
intl: intlShape.isRequired,
};
openLink = () => {
const {link} = this.props;
if (link && Linking.canOpenURL(link)) {
Linking.openURL(link);
const {intl} = this.context;
if (link) {
const onError = () => {
Alert.alert(
intl.formatMessage({
id: 'mobile.link.error.title',
defaultMessage: 'Error',
}),
intl.formatMessage({
id: 'mobile.link.error.text',
defaultMessage: 'Unable to open the link.',
}),
);
};
tryOpenURL(link, onError);
}
};

View File

@@ -2,11 +2,13 @@
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import {Linking, Text, View} from 'react-native';
import {Alert, Text, View} from 'react-native';
import {intlShape} from 'react-intl';
import PropTypes from 'prop-types';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import Markdown from 'app/components/markdown';
import Markdown from '@components/markdown';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
export default class AttachmentTitle extends PureComponent {
static propTypes = {
@@ -15,10 +17,29 @@ export default class AttachmentTitle extends PureComponent {
value: PropTypes.string,
};
static contextTypes = {
intl: intlShape.isRequired,
};
openLink = () => {
const {link} = this.props;
if (link && Linking.canOpenURL(link)) {
Linking.openURL(link);
const {intl} = this.context;
if (link) {
const onError = () => {
Alert.alert(
intl.formatMessage({
id: 'mobile.link.error.title',
defaultMessage: 'Error',
}),
intl.formatMessage({
id: 'mobile.link.error.text',
defaultMessage: 'Unable to open the link.',
}),
);
};
tryOpenURL(link, onError);
}
};

View File

@@ -3,8 +3,9 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Linking, Text, View} from 'react-native';
import {Alert, Text, View} from 'react-native';
import FastImage from 'react-native-fast-image';
import {intlShape} from 'react-intl';
import parseUrl from 'url-parse';
import {TABLET_WIDTH} from '@components/sidebars/drawer_layout';
@@ -14,6 +15,7 @@ import {generateId} from '@utils/file';
import {openGalleryAtIndex, calculateDimensions} from '@utils/images';
import {getNearestPoint} from '@utils/opengraph';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
const MAX_IMAGE_HEIGHT = 150;
const VIEWPORT_IMAGE_OFFSET = 93;
@@ -31,6 +33,10 @@ export default class PostAttachmentOpenGraph extends PureComponent {
theme: PropTypes.object.isRequired,
};
static contextTypes = {
intl: intlShape.isRequired,
};
constructor(props) {
super(props);
@@ -153,7 +159,21 @@ export default class PostAttachmentOpenGraph extends PureComponent {
};
goToLink = () => {
Linking.openURL(this.props.link);
const {intl} = this.context;
const onError = () => {
Alert.alert(
intl.formatMessage({
id: 'mobile.link.error.title',
defaultMessage: 'Error',
}),
intl.formatMessage({
id: 'mobile.link.error.text',
defaultMessage: 'Unable to open the link.',
}),
);
};
tryOpenURL(this.props.link, onError);
};
handlePreviewImage = () => {

View File

@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
import {
Alert,
Image,
Linking,
Platform,
StyleSheet,
StatusBar,
@@ -23,7 +22,7 @@ import CustomPropTypes from '@constants/custom_prop_types';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {generateId} from '@utils/file';
import {calculateDimensions, getViewPortWidth, openGalleryAtIndex} from '@utils/images';
import {getYouTubeVideoId, isImageLink, isYoutubeLink} from '@utils/url';
import {getYouTubeVideoId, isImageLink, isYoutubeLink, tryOpenURL} from '@utils/url';
const MAX_YOUTUBE_IMAGE_HEIGHT = 202;
const MAX_YOUTUBE_IMAGE_WIDTH = 360;
@@ -272,7 +271,21 @@ export default class PostBodyAdditionalContent extends ImageViewPort {
startTime,
}).catch(this.playYouTubeVideoError);
} else {
Linking.openURL(videoLink);
const {intl} = this.context;
const onError = () => {
Alert.alert(
intl.formatMessage({
id: 'mobile.link.error.title',
defaultMessage: 'Error',
}),
intl.formatMessage({
id: 'mobile.link.error.text',
defaultMessage: 'Unable to open the link.',
}),
);
};
tryOpenURL(videoLink, onError);
}
}
};

View File

@@ -14,6 +14,7 @@ import QuickActions from '@components/post_draft/quick_actions';
import SendAction from '@components/post_draft/send_action';
import Typing from '@components/post_draft/typing';
import Uploads from '@components/post_draft/uploads';
import DEVICE from '@constants/device';
import {CHANNEL_POST_TEXTBOX_CURSOR_CHANGE, CHANNEL_POST_TEXTBOX_VALUE_CHANGE, IS_REACTION_REGEX} from '@constants/post_draft';
import {NOTIFY_ALL_MEMBERS} from '@constants/view';
import EventEmitter from '@mm-redux/utils/event_emitter';
@@ -23,7 +24,6 @@ import {confirmOutOfOfficeDisabled} from '@utils/status';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
const AUTOCOMPLETE_MARGIN = 20;
const AUTOCOMPLETE_MAX_HEIGHT = 200;
const HW_SHIFT_ENTER_TEXT = Platform.OS === 'ios' ? '\n' : '';
const HW_EVENT_IN_SCREEN = ['Channel', 'Thread'];
@@ -44,6 +44,7 @@ export default class DraftInput extends PureComponent {
getChannelTimezones: PropTypes.func.isRequired,
handleClearFiles: PropTypes.func.isRequired,
handleClearFailedFiles: PropTypes.func.isRequired,
handleGotoLocation: PropTypes.func.isRequired,
isLandscape: PropTypes.bool.isRequired,
isTimezoneEnabled: PropTypes.bool,
maxMessageLength: PropTypes.number.isRequired,
@@ -299,7 +300,7 @@ export default class DraftInput extends PureComponent {
return;
}
const {error} = await executeCommand(msg, channelId, rootId);
const {data, error} = await executeCommand(msg, channelId, rootId);
this.setState({sendingMessage: false});
if (error) {
@@ -310,6 +311,10 @@ export default class DraftInput extends PureComponent {
this.setInputValue('');
this.input.current.changeDraft('');
if (data.goto_location) {
this.props.handleGotoLocation(data.goto_location, this.context.intl);
}
};
sendMessage = (value = '') => {
@@ -425,6 +430,17 @@ export default class DraftInput extends PureComponent {
theme={theme}
registerTypingAnimation={registerTypingAnimation}
/>
{Platform.OS === 'android' &&
<Autocomplete
cursorPositionEvent={cursorPositionEvent}
maxHeight={Math.min(this.state.top - AUTOCOMPLETE_MARGIN, DEVICE.AUTOCOMPLETE_MAX_HEIGHT)}
onChangeText={this.handleInputQuickAction}
valueEvent={valueEvent}
rootId={rootId}
channelId={channelId}
offsetY={0}
/>
}
<SafeAreaView
edges={['left', 'right']}
onLayout={this.handleLayout}
@@ -480,16 +496,6 @@ export default class DraftInput extends PureComponent {
</View>
</ScrollView>
</SafeAreaView>
{Platform.OS === 'android' &&
<Autocomplete
cursorPositionEvent={cursorPositionEvent}
maxHeight={Math.min(this.state.top - AUTOCOMPLETE_MARGIN, AUTOCOMPLETE_MAX_HEIGHT)}
onChangeText={this.handleInputQuickAction}
valueEvent={valueEvent}
rootId={rootId}
channelId={channelId}
/>
}
</>
);
}

View File

@@ -80,6 +80,7 @@ describe('DraftInput', () => {
},
membersCount: 10,
addRecentUsedEmojisInMessage: jest.fn(),
handleGotoLocation: jest.fn(),
};
const ref = React.createRef();

View File

@@ -8,6 +8,7 @@ import {addReactionToLatestPost, addRecentUsedEmojisInMessage} from '@actions/vi
import {handleClearFiles, handleClearFailedFiles} from '@actions/views/file_upload';
import {MAX_MESSAGE_LENGTH_FALLBACK} from '@constants/post_draft';
import {getChannelTimezones, getChannelMemberCountsByGroup} from '@mm-redux/actions/channels';
import {handleGotoLocation} from '@mm-redux/actions/integrations';
import {createPost} from '@mm-redux/actions/posts';
import {setStatus} from '@mm-redux/actions/users';
import {General, Permissions} from '@mm-redux/constants';
@@ -99,6 +100,7 @@ const mapDispatchToProps = {
getChannelTimezones,
handleClearFiles,
handleClearFailedFiles,
handleGotoLocation,
setStatus,
getChannelMemberCountsByGroup,
addRecentUsedEmojisInMessage,

View File

@@ -8,6 +8,7 @@ import {intlShape} from 'react-intl';
import PasteableTextInput from '@components/pasteable_text_input';
import {NavigationTypes} from '@constants';
import DEVICE from '@constants/device';
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {t} from '@utils/i18n';
@@ -271,7 +272,7 @@ export default class PostInput extends PureComponent {
const {testID, channelDisplayName, isLandscape, theme} = this.props;
const style = getStyleSheet(theme);
const placeholder = this.getPlaceHolder();
let maxHeight = 150;
let maxHeight = DEVICE.POST_INPUT_MAX_HEIGHT;
if (isLandscape) {
maxHeight = 88;

View File

@@ -9,6 +9,7 @@ import {intlShape} from 'react-intl';
import {Posts} from '@mm-redux/constants';
import EventEmitter from '@mm-redux/utils/event_emitter';
import * as PostListUtils from '@mm-redux/utils/post_list';
import {errorBadChannel} from '@utils/draft';
import CombinedUserActivityPost from 'app/components/combined_user_activity_post';
import Post from 'app/components/post';
@@ -17,8 +18,6 @@ import mattermostManaged from 'app/mattermost_managed';
import {makeExtraData} from 'app/utils/list_view';
import {matchDeepLink} from 'app/utils/url';
import telemetry from 'app/telemetry';
import {alertErrorWithFallback} from 'app/utils/general';
import {t} from 'app/utils/i18n';
import DateHeader from './date_header';
import NewMessagesDivider from './new_messages_divider';
@@ -188,7 +187,8 @@ export default class PostList extends PureComponent {
if (match) {
if (match.type === DeepLinkTypes.CHANNEL) {
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, this.permalinkBadChannel);
const {intl} = this.context;
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, errorBadChannel(intl));
} else if (match.type === DeepLinkTypes.PERMALINK) {
this.handlePermalinkPress(match.postId, match.teamName);
}
@@ -277,16 +277,6 @@ export default class PostList extends PureComponent {
});
};
permalinkBadChannel = () => {
const {intl} = this.context;
const message = {
id: t('mobile.server_link.unreachable_channel.error'),
defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.',
};
alertErrorWithFallback(intl, {}, message);
};
renderItem = ({item, index}) => {
const {
testID,

View File

@@ -14,6 +14,7 @@ jest.useFakeTimers();
jest.mock('react-intl');
describe('PostList', () => {
const formatMessage = jest.fn();
const serverURL = 'https://server-url.fake';
const deeplinkRoot = 'mattermost://server-url.fake';
@@ -61,6 +62,7 @@ describe('PostList', () => {
test('setting channel deep link', () => {
const wrapper = shallow(
<PostList {...baseProps}/>,
{context: {intl: {formatMessage}}},
);
wrapper.setProps({deepLinkURL: deepLinks.channel});

View File

@@ -24,7 +24,7 @@ function makeMapStateToProps() {
const getChannel = makeGetChannel();
return (state, ownProps) => {
const channel = ownProps.channel || getChannel(state, {id: ownProps.channelId});
const channel = ownProps.channel || getChannel(state, {id: ownProps.channelId}) || {};
const member = getMyChannelMember(state, channel.id);
const currentUserId = getCurrentUserId(state);
const channelDraft = getDraftForChannel(state, channel.id);

View File

@@ -3,6 +3,8 @@
export default {
CHANNEL: 'channel',
DMCHANNEL: 'dmchannel',
GROUPCHANNEL: 'groupchannel',
PERMALINK: 'permalink',
OTHER: 'other',
};

View File

@@ -14,13 +14,19 @@ const deviceTypes = keyMirror({
STATUSBAR_HEIGHT_CHANGED: null,
});
const isPhoneWithInsets = Platform.OS === 'ios' && DeviceInfo.hasNotch();
const isTablet = DeviceInfo.isTablet();
const isIPhone12Mini = DeviceInfo.getModel() === 'iPhone 12 mini';
export default {
...deviceTypes,
DOCUMENTS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Documents`,
IMAGES_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Images`,
IS_IPHONE_WITH_INSETS: Platform.OS === 'ios' && DeviceInfo.hasNotch(),
IS_IPHONE_WITH_INSETS: isPhoneWithInsets,
IS_TABLET: DeviceInfo.isTablet(),
VIDEOS_PATH: `${RNFetchBlobFS.dirs.CacheDir}/Videos`,
PERMANENT_SIDEBAR_SETTINGS: '@PERMANENT_SIDEBAR_SETTINGS',
TABLET_WIDTH: 250,
AUTOCOMPLETE_MAX_HEIGHT: (isPhoneWithInsets && !isIPhone12Mini) || isTablet ? 200 : 145,
POST_INPUT_MAX_HEIGHT: (isPhoneWithInsets && !isIPhone12Mini) || isTablet ? 150 : 88,
};

View File

@@ -202,7 +202,6 @@ class PushNotifications {
onRemoteNotificationsRegistered = (event: Registered) => {
if (!this.configured) {
this.configured = true;
const {deviceToken} = event;
let prefix;
@@ -217,11 +216,22 @@ class PushNotifications {
EphemeralStore.deviceToken = `${prefix}:${deviceToken}`;
if (Store.redux) {
this.configured = true;
const dispatch = Store.redux.dispatch as DispatchFunc;
waitForHydration(Store.redux, () => {
this.requestNotificationReplyPermissions();
dispatch(setDeviceToken(EphemeralStore.deviceToken));
});
} else {
// The redux store is not ready, so we retry it to set the
// token to prevent sessions being registered without a device id
// This code may be executed on fast devices cause the token registration
// is faster than the redux store configuration.
// Note: Should not be needed once WDB is implemented
const remoteTimeout = setTimeout(() => {
clearTimeout(remoteTimeout);
this.onRemoteNotificationsRegistered(event);
}, 200);
}
}
};

View File

@@ -1,5 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Alert} from 'react-native';
import {IntegrationTypes} from '@mm-redux/action_types';
import {General} from '../constants';
import {Client4} from '@mm-redux/client';
@@ -14,6 +17,17 @@ import {Command, DialogSubmission, IncomingWebhook, OAuthApp, OutgoingWebhook} f
import {logError} from './errors';
import {bindClientFunc, forceLogoutIfNecessary, requestSuccess, requestFailure} from './helpers';
import {makeGroupMessageVisibleIfNecessary} from './preferences';
import {handleSelectChannel, handleSelectChannelByName, loadChannelsByTeamName} from '@actions/views/channel';
import {makeDirectChannel} from '@actions/views/more_dms';
import {showPermalink} from '@actions/views/permalink';
import {DeepLinkTypes} from '@constants';
import {getUserByUsername} from '@mm-redux/actions/users';
import {getConfig, getCurrentUrl} from '@mm-redux/selectors/entities/general';
import * as DraftUtils from '@utils/draft';
import {permalinkBadTeam} from '@utils/general';
import {getURLAndMatch, tryOpenURL} from '@utils/url';
export function createIncomingHook(hook: IncomingWebhook): ActionFunc {
return bindClientFunc({
clientFunc: Client4.createIncomingWebhook,
@@ -399,3 +413,64 @@ export function submitInteractiveDialog(submission: DialogSubmission): ActionFun
return {data};
};
}
export function handleGotoLocation(href: string, intl: any): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState();
const config = getConfig(state);
const {url, match} = await getURLAndMatch(href, getCurrentUrl(state), config.SiteURL);
if (match) {
switch (match.type) {
case DeepLinkTypes.CHANNEL:
dispatch(handleSelectChannelByName(match.channelName, match.teamName, () => DraftUtils.errorBadChannel(intl)));
break;
case DeepLinkTypes.PERMALINK: {
const {error} = await dispatch(loadChannelsByTeamName(match.teamName, () => permalinkBadTeam(intl)));
if (!error && match.postId) {
dispatch(showPermalink(intl, match.teamName, match.postId));
}
break;
}
case DeepLinkTypes.DMCHANNEL: {
if (!match.userName) {
DraftUtils.errorBadUser(intl);
return {data: false};
}
const {data} = await dispatch(getUserByUsername(match.userName));
if (!data) {
DraftUtils.errorBadUser(intl);
return {data: false};
}
dispatch(makeDirectChannel(data.id));
break;
}
case DeepLinkTypes.GROUPCHANNEL:
if (!match.id) {
DraftUtils.errorBadChannel(intl);
return {data: false};
}
dispatch(makeGroupMessageVisibleIfNecessary(match.id));
dispatch(handleSelectChannel(match.id));
break;
}
} else {
const {formatMessage} = this.context.intl;
const onError = () => Alert.alert(
formatMessage({
id: 'mobile.server_link.error.title',
defaultMessage: 'Link Error',
}),
formatMessage({
id: 'mobile.server_link.error.text',
defaultMessage: 'The link could not be found on this server.',
}),
);
tryOpenURL(url, onError);
}
return {data: true};
};
}

View File

@@ -4,7 +4,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Linking,
Alert,
ScrollView,
Text,
TouchableOpacity,
@@ -12,6 +12,7 @@ import {
} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import {SafeAreaView} from 'react-native-safe-area-context';
import {intlShape} from 'react-intl';
import Config from '@assets/config';
import CompassIcon from '@components/compass_icon';
@@ -19,6 +20,7 @@ import FormattedText from '@components/formatted_text';
import StatusBar from '@components/status_bar';
import AboutLinks from '@constants/about_links';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
const MATTERMOST_BUNDLE_IDS = ['com.mattermost.rnbeta', 'com.mattermost.rn'];
@@ -29,28 +31,50 @@ export default class About extends PureComponent {
theme: PropTypes.object.isRequired,
};
static contextTypes = {
intl: intlShape.isRequired,
};
openURL = (url) => {
const {intl} = this.context;
const onError = () => {
Alert.alert(
intl.formatMessage({
id: 'mobile.link.error.title',
defaultMessage: 'Error',
}),
intl.formatMessage({
id: 'mobile.link.error.text',
defaultMessage: 'Unable to open the link.',
}),
);
};
tryOpenURL(url, onError);
};
handleAboutTeam = () => {
Linking.openURL(Config.AboutTeamURL);
this.openURL(Config.AboutTeamURL);
};
handleAboutEnterprise = () => {
Linking.openURL(Config.AboutEnterpriseURL);
this.openURL(Config.AboutEnterpriseURL);
};
handlePlatformNotice = () => {
Linking.openURL(Config.PlatformNoticeURL);
this.openURL(Config.PlatformNoticeURL);
};
handleMobileNotice = () => {
Linking.openURL(Config.MobileNoticeURL);
this.openURL(Config.MobileNoticeURL);
};
handleTermsOfService = () => {
Linking.openURL(AboutLinks.TERMS_OF_SERVICE);
this.openURL(AboutLinks.TERMS_OF_SERVICE);
};
handlePrivacyPolicy = () => {
Linking.openURL(AboutLinks.PRIVACY_POLICY);
this.openURL(AboutLinks.PRIVACY_POLICY);
}
render() {

View File

@@ -7,13 +7,14 @@ import {SafeAreaView} from 'react-native-safe-area-context';
import LocalConfig from '@assets/config';
import AnnouncementBanner from 'app/components/announcement_banner';
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from '@components/autocomplete';
import Autocomplete from '@components/autocomplete';
import InteractiveDialogController from '@components/interactive_dialog_controller';
import NetworkIndicator from '@components/network_indicator';
import PostDraft from '@components/post_draft';
import MainSidebar from '@components/sidebars/main';
import SettingsSidebar from '@components/sidebars/settings';
import StatusBar from '@components/status_bar';
import DEVICE from '@constants/device';
import {ACCESSORIES_CONTAINER_NATIVE_ID, CHANNEL_POST_TEXTBOX_CURSOR_CHANGE, CHANNEL_POST_TEXTBOX_VALUE_CHANGE} from '@constants/post_draft';
import {makeStyleSheetFromTheme} from '@utils/theme';
@@ -98,6 +99,16 @@ export default class ChannelIOS extends ChannelBase {
{component}
</SafeAreaView>
{indicators}
<View nativeID={ACCESSORIES_CONTAINER_NATIVE_ID}>
<Autocomplete
maxHeight={DEVICE.AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.handleAutoComplete}
cursorPositionEvent={CHANNEL_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
channelId={currentChannelId}
offsetY={0}
/>
</View>
{renderDraftArea &&
<PostDraft
testID='channel.post_draft'
@@ -110,15 +121,6 @@ export default class ChannelIOS extends ChannelBase {
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
/>
}
<View nativeID={ACCESSORIES_CONTAINER_NATIVE_ID}>
<Autocomplete
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.handleAutoComplete}
cursorPositionEvent={CHANNEL_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={CHANNEL_POST_TEXTBOX_VALUE_CHANGE}
channelId={currentChannelId}
/>
</View>
</>
);

View File

@@ -7,7 +7,6 @@ import PropTypes from 'prop-types';
import {
Alert,
Image,
Linking,
ScrollView,
TouchableOpacity,
View,
@@ -21,6 +20,7 @@ import StatusBar from '@components/status_bar';
import {UpgradeTypes} from '@constants';
import {checkUpgradeType, isUpgradeAvailable} from '@utils/client_upgrade';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
export default class ClientUpgrade extends PureComponent {
static propTypes = {
@@ -104,11 +104,7 @@ export default class ClientUpgrade extends PureComponent {
const {downloadLink} = this.props;
const {intl} = this.context;
Linking.canOpenURL(downloadLink).then((supported) => {
if (supported) {
return Linking.openURL(downloadLink);
}
const onError = () => {
Alert.alert(
intl.formatMessage({
id: 'mobile.client_upgrade.download_error.title',
@@ -119,9 +115,9 @@ export default class ClientUpgrade extends PureComponent {
defaultMessage: 'An error occurred while trying to open the download link.',
}),
);
};
return false;
});
tryOpenURL(downloadLink, onError);
};
renderMustUpgrade() {

View File

@@ -12,11 +12,12 @@ import {Navigation} from 'react-native-navigation';
import {KeyboardTrackingView} from 'react-native-keyboard-tracking-view';
import {SafeAreaView} from 'react-native-safe-area-context';
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from 'app/components/autocomplete';
import Autocomplete from 'app/components/autocomplete';
import ErrorText from 'app/components/error_text';
import Loading from 'app/components/loading';
import StatusBar from 'app/components/status_bar';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import DEVICE from '@constants/device';
import {switchKeyboardForCodeBlocks} from 'app/utils/markdown';
import {
changeOpacity,
@@ -287,7 +288,7 @@ export default class EditPost extends PureComponent {
<KeyboardTrackingView style={autocompleteStyles}>
<Autocomplete
cursorPosition={this.state.cursorPosition}
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
maxHeight={DEVICE.AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.onPostChangeText}
value={message}
nestedScrollEnabled={true}

View File

@@ -5,7 +5,7 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape, injectIntl} from 'react-intl';
import {
Linking,
Alert,
Platform,
ScrollView,
View,
@@ -21,7 +21,7 @@ import SettingsItem from '@screens/settings/settings_item';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {isValidUrl} from '@utils/url';
import {isValidUrl, tryOpenURL} from '@utils/url';
class Settings extends PureComponent {
static propTypes = {
@@ -135,28 +135,48 @@ class Settings extends PureComponent {
});
openErrorEmail = preventDoubleTap(() => {
const {config} = this.props;
const {config, intl} = this.props;
const recipient = config.SupportEmail;
const subject = `Problem with ${config.SiteName} React Native app`;
const mailTo = `mailto:${recipient}?subject=${subject}&body=${this.errorEmailBody()}`;
Linking.canOpenURL(mailTo).then((supported) => {
if (supported) {
Linking.openURL(mailTo);
this.props.actions.clearErrors();
}
});
const onSuccess = () => {
this.props.actions.clearErrors();
};
const onError = () => {
Alert.alert(
intl.formatMessage({
id: 'mobile.mailTo.error.title',
defaultMessage: 'Error',
}),
intl.formatMessage({
id: 'mobile.mailTo.error.text',
defaultMessage: 'Unable to open an email client.',
}),
);
};
tryOpenURL(mailTo, onError, onSuccess);
});
openHelp = preventDoubleTap(() => {
const {config} = this.props;
const {config, intl} = this.props;
const link = config.HelpLink ? config.HelpLink.toLowerCase() : '';
Linking.canOpenURL(link).then((supported) => {
if (supported) {
Linking.openURL(link);
}
});
const onError = () => {
Alert.alert(
intl.formatMessage({
id: 'mobile.link.error.title',
defaultMessage: 'Error',
}),
intl.formatMessage({
id: 'mobile.link.error.text',
defaultMessage: 'Unable to open the link.',
}),
);
};
tryOpenURL(link, onError);
});
render() {

View File

@@ -52,6 +52,19 @@ exports[`thread should match snapshot, has root post 1`] = `
</ForwardRef(AnimatedComponentWrapper)>
</React.Fragment>
</Connect(SafeArea)>
<View
nativeID="threadAccessoriesContainer"
>
<Connect(Autocomplete)
channelId="channel_id"
cursorPositionEvent="onThreadTextBoxCursorChange"
maxHeight={200}
offsetY={0}
onChangeText={[Function]}
rootId="root_id"
valueEvent="onThreadTextBoxValueChange"
/>
</View>
<Connect(PostDraft)
accessoriesContainerID="threadAccessoriesContainer"
channelId="channel_id"
@@ -63,18 +76,6 @@ exports[`thread should match snapshot, has root post 1`] = `
testID="thread.post_draft"
valueEvent="onThreadTextBoxValueChange"
/>
<View
nativeID="threadAccessoriesContainer"
>
<Connect(Autocomplete)
channelId="channel_id"
cursorPositionEvent="onThreadTextBoxCursorChange"
maxHeight={200}
onChangeText={[Function]}
rootId="root_id"
valueEvent="onThreadTextBoxValueChange"
/>
</View>
</React.Fragment>
`;
@@ -106,6 +107,7 @@ exports[`thread should match snapshot, no root post, loading 1`] = `
channelId="channel_id"
cursorPositionEvent="onThreadTextBoxCursorChange"
maxHeight={200}
offsetY={0}
onChangeText={[Function]}
rootId="root_id"
valueEvent="onThreadTextBoxValueChange"
@@ -190,6 +192,7 @@ exports[`thread should match snapshot, render footer 3`] = `
channelId="channel_id"
cursorPositionEvent="onThreadTextBoxCursorChange"
maxHeight={200}
offsetY={0}
onChangeText={[Function]}
rootId="root_id"
valueEvent="onThreadTextBoxValueChange"

View File

@@ -4,12 +4,13 @@
import React from 'react';
import {Animated, View} from 'react-native';
import Autocomplete, {AUTOCOMPLETE_MAX_HEIGHT} from '@components/autocomplete';
import Autocomplete from '@components/autocomplete';
import Loading from '@components/loading';
import PostList from '@components/post_list';
import PostDraft from '@components/post_draft';
import SafeAreaView from '@components/safe_area_view';
import StatusBar from '@components/status_bar';
import DEVICE from '@constants/device';
import {THREAD} from '@constants/screen';
import {getLastPostIndex} from '@mm-redux/utils/post_list';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@@ -95,17 +96,18 @@ export default class ThreadIOS extends ThreadBase {
<StatusBar/>
{content}
</SafeAreaView>
{postDraft}
<View nativeID={ACCESSORIES_CONTAINER_NATIVE_ID}>
<Autocomplete
maxHeight={AUTOCOMPLETE_MAX_HEIGHT}
maxHeight={DEVICE.AUTOCOMPLETE_MAX_HEIGHT}
onChangeText={this.handleAutoComplete}
cursorPositionEvent={THREAD_POST_TEXTBOX_CURSOR_CHANGE}
valueEvent={THREAD_POST_TEXTBOX_VALUE_CHANGE}
rootId={rootId}
channelId={channelId}
offsetY={0}
/>
</View>
{postDraft}
</React.Fragment>
);
}

View File

@@ -4,10 +4,10 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Alert,
ScrollView,
Text,
View,
Linking,
} from 'react-native';
import {intlShape} from 'react-intl';
import {Navigation} from 'react-native-navigation';
@@ -30,6 +30,7 @@ import {getUserCurrentTimezone} from '@mm-redux/utils/timezone_utils';
import {alertErrorWithFallback} from '@utils/general';
import {t} from '@utils/i18n';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
import {isGuest} from '@utils/users';
import UserProfileRow from './user_profile_row';
@@ -229,11 +230,25 @@ export default class UserProfile extends PureComponent {
handleLinkPress = (link) => {
const username = this.props.user.username;
const email = this.props.user.email;
const {intl} = this.context;
return () => {
let hydrated = link.replace(/{email}/, email);
hydrated = hydrated.replace(/{username}/, username);
Linking.openURL(hydrated);
const onError = () => {
Alert.alert(
intl.formatMessage({
id: 'mobile.link.error.title',
defaultMessage: 'Error',
}),
intl.formatMessage({
id: 'mobile.link.error.text',
defaultMessage: 'Unable to open the link.',
}),
);
};
tryOpenURL(hydrated, onError);
};
};

View File

@@ -6,6 +6,8 @@ import {createSelector} from 'reselect';
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
import {getChannelByName} from '@mm-redux/selectors/entities/channels';
import {getTeamByName} from '@mm-redux/selectors/entities/teams';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {isArchivedChannel} from '@mm-redux/utils/channel_utils';
const getOtherUserIdForDm = createSelector(
(state, channel) => channel,
@@ -46,5 +48,15 @@ const getChannel = (state, channelName) => getChannelByName(state, channelName);
export const getChannelReachable = createSelector(
getTeam,
getChannel,
(team, channel) => team && channel,
getConfig,
(team, channel, config) => {
if (!(team && channel)) {
return false;
}
const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
if (isArchivedChannel(channel) && !viewArchivedChannels) {
return false;
}
return true;
},
);

View File

@@ -6,6 +6,8 @@ import {Alert} from 'react-native';
import {AT_MENTION_REGEX_GLOBAL, CODE_REGEX} from '@constants/autocomplete';
import {NOTIFY_ALL_MEMBERS} from '@constants/view';
import {General} from '@mm-redux/constants';
import {alertErrorWithFallback} from '@utils/general';
import {t} from '@utils/i18n';
export function alertAttachmentFail(formatMessage, accept, cancel) {
Alert.alert(
@@ -79,6 +81,24 @@ export function alertSendToGroups(formatMessage, notifyAllMessage, accept, cance
);
}
export function errorBadChannel(intl) {
const message = {
id: t('mobile.server_link.unreachable_channel.error'),
defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.',
};
alertErrorWithFallback(intl, {}, message);
}
export function errorBadUser(intl) {
const message = {
id: t('mobile.server_link.unreachable_user.error'),
defaultMessage: 'This link belongs to a deleted user.',
};
alertErrorWithFallback(intl, {}, message);
}
export function alertSlashCommandFailed(formatMessage, error) {
Alert.alert(
formatMessage({

View File

@@ -283,7 +283,24 @@ export const hashCode = (str) => {
export function getLocalFilePathFromFile(dir, file) {
if (dir) {
if (file?.name) {
const [filename, extension] = file.name.split('.');
let extension = file.extension;
let filename = file.name;
if (!extension) {
extension = getExtensionFromMime(file.mime_type);
}
if (extension && filename.includes(`.${extension}`)) {
filename = filename.replace(`.${extension}`, '');
} else {
const fileParts = file.name.split('.');
if (fileParts.length > 1) {
extension = fileParts.pop();
filename = fileParts.join('.');
}
}
return `${dir}/${filename}-${hashCode(file.id)}.${extension}`;
} else if (file?.id && file?.extension) {
return `${dir}/${file.id}.${file.extension}`;

View File

@@ -118,7 +118,7 @@ describe('getExtensionFromContentDisposition', () => {
it('should return the path for the document file', () => {
const file = {
id: generateId(),
name: 'Some other doocument.txt',
name: 'Some other document.txt',
extension: 'txt',
};
@@ -126,4 +126,15 @@ describe('getExtensionFromContentDisposition', () => {
const [filename] = file.name.split('.');
expect(localFile).toBe(`${DeviceTypes.DOCUMENTS_PATH}/${filename}-${hashCode(file.id)}.${file.extension}`);
});
it('should return the path for the document file including multiple dots in the filename', () => {
const file = {
id: generateId(),
name: 'Some.other.document.txt',
extension: 'txt',
};
const expectedFilename = 'Some.other.document';
const localFile = getLocalPath(file);
expect(localFile).toBe(`${DeviceTypes.DOCUMENTS_PATH}/${expectedFilename}-${hashCode(file.id)}.${file.extension}`);
});
});

View File

@@ -1,8 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Alert, Linking, AlertButton} from 'react-native';
import {Alert, AlertButton} from 'react-native';
import {ViewTypes} from '@constants';
import {tryOpenURL} from '@utils/url';
interface FormatObjectType {
id: string;
@@ -36,9 +37,20 @@ function unsupportedServerAdminAlert(formatMessage: FormatMessageType) {
style: 'cancel',
onPress: () => {
const url = 'https://mattermost.com/blog/support-for-esr-5-9-has-ended/';
if (Linking.canOpenURL(url)) {
Linking.openURL(url);
}
const onError = () => {
Alert.alert(
formatMessage({
id: 'mobile.link.error.title',
defaultMessage: 'Error',
}),
formatMessage({
id: 'mobile.link.error.text',
defaultMessage: 'Unable to open the link.',
}),
);
};
tryOpenURL(url, onError);
},
};
const buttons: AlertButton[] = [cancel, learnMore];

View File

@@ -1,12 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Linking} from 'react-native';
import {latinise} from './latinise.js';
import {escapeRegex} from './markdown';
import {Files} from '@mm-redux/constants';
import {getCurrentServerUrl} from '@init/credentials';
import {DeepLinkTypes} from 'app/constants';
import {DeepLinkTypes} from '@constants';
import {emptyFunction} from '@utils/general';
const ytRegex = /(?:http|https):\/\/(?:www\.|m\.)?(?:(?:youtube\.com\/(?:(?:v\/)|(?:(?:watch|embed\/watch)(?:\/|.*v=))|(?:embed\/)|(?:user\/[^/]+\/u\/[0-9]\/)))|(?:youtu\.be\/))([^#&?]*)/;
@@ -128,6 +132,16 @@ export function matchDeepLink(url, serverURL, siteURL) {
return {type: DeepLinkTypes.PERMALINK, teamName: match[1], postId: match[2]};
}
match = new RegExp(linkRoot + '\\/([^\\/]+)\\/messages\\/@(\\S+)').exec(urlToMatch);
if (match) {
return {type: DeepLinkTypes.DMCHANNEL, teamName: match[1], userName: match[2]};
}
match = new RegExp(linkRoot + '\\/([^\\/]+)\\/messages\\/(\\S+)').exec(urlToMatch);
if (match) {
return {type: DeepLinkTypes.GROUPCHANNEL, teamName: match[1], id: match[2]};
}
return null;
}
@@ -152,3 +166,26 @@ export function getYouTubeVideoId(link) {
return '';
}
export async function getURLAndMatch(href, serverURL, siteURL) {
const url = normalizeProtocol(href);
if (!url) {
return {};
}
let serverUrl = serverURL;
if (!serverUrl) {
serverUrl = await getCurrentServerUrl();
}
const match = matchDeepLink(url, serverURL, siteURL);
return {url, match};
}
export function tryOpenURL(url, onError = emptyFunction, onSuccess = emptyFunction) {
Linking.openURL(url).
then(onSuccess).
catch(onError);
}

View File

@@ -1,7 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as UrlUtils from 'app/utils/url';
import {Linking} from 'react-native';
import * as UrlUtils from '@utils/url';
/* eslint-disable max-nested-callbacks */
@@ -131,4 +133,28 @@ describe('UrlUtils', () => {
});
}
});
describe('tryOpenUrl', () => {
const url = 'https://some.url.com';
it('should call onSuccess when Linking.openURL succeeds', async () => {
Linking.openURL.mockResolvedValueOnce();
const onError = jest.fn();
const onSuccess = jest.fn();
await UrlUtils.tryOpenURL(url, onError, onSuccess);
expect(onError).not.toHaveBeenCalled();
expect(onSuccess).toHaveBeenCalledTimes(1);
});
it('should call onError when Linking.openURL fails', async () => {
Linking.openURL.mockRejectedValueOnce();
const onError = jest.fn();
const onSuccess = jest.fn();
await UrlUtils.tryOpenURL(url, onError, onSuccess);
expect(onError).toHaveBeenCalledTimes(1);
expect(onSuccess).not.toHaveBeenCalled();
});
});
});

View File

@@ -291,12 +291,16 @@
"mobile.ios.photos_permission_denied_description": "Upload photos and videos to your Mattermost instance or save them to your device. Open Settings to grant Mattermost Read and Write access to your photo and video library.",
"mobile.ios.photos_permission_denied_title": "{applicationName} would like to access your photos",
"mobile.join_channel.error": "We couldn't join the channel {displayName}. Please check your connection and try again.",
"mobile.link.error.text": "Unable to open the link.",
"mobile.link.error.title": "Error",
"mobile.loading_channels": "Loading Channels...",
"mobile.loading_members": "Loading Members...",
"mobile.loading_options": "Loading Options...",
"mobile.loading_posts": "Loading messages...",
"mobile.login_options.choose_title": "Choose your login method",
"mobile.long_post_title": "{channelName} - Post",
"mobile.mailTo.error.text": "Unable to open an email client.",
"mobile.mailTo.error.title": "Error",
"mobile.managed.blocked_by": "Blocked by {vendor}",
"mobile.managed.exit": "Exit",
"mobile.managed.jailbreak": "Jailbroken devices are not trusted by {vendor}, please exit the app.",
@@ -467,6 +471,7 @@
"mobile.server_link.error.title": "Link Error",
"mobile.server_link.unreachable_channel.error": "This link belongs to a deleted channel or to a channel to which you do not have access.",
"mobile.server_link.unreachable_team.error": "This link belongs to a deleted team or to a team to which you do not have access.",
"mobile.server_link.unreachable_user.error": "This link belongs to a deleted user.",
"mobile.server_ssl.error.text": "The certificate from {host} is not trusted.\n\nPlease contact your System Administrator to resolve the certificate issues and allow connections to this server.",
"mobile.server_ssl.error.title": "Untrusted Certificate",
"mobile.server_upgrade.alert_description": "This server version is unsupported and users will be exposed to compatibility issues that cause crashes or severe bugs breaking core functionality of the app. Upgrading to server version {serverVersion} or later is required.",

View File

@@ -198,6 +198,7 @@
"mobile.channel_list.members": "メンバー",
"mobile.channel_list.not_member": "非メンバー",
"mobile.channel_list.unreads": "未読",
"mobile.channel_loader.still_loading": "コンテンツを読み込んでいます...",
"mobile.channel_members.add_members_alert": "チャンネルに追加するメンバーを少なくとも一人選択してください。",
"mobile.client_upgrade": "アプリを更新する",
"mobile.client_upgrade.can_upgrade_subtitle": "新しいバージョンが利用できます。",

View File

@@ -198,6 +198,7 @@
"mobile.channel_list.members": "MEMBERS",
"mobile.channel_list.not_member": "NOT A MEMBER",
"mobile.channel_list.unreads": "읽지 않음",
"mobile.channel_loader.still_loading": "콘텐츠를 불러오는 중...",
"mobile.channel_members.add_members_alert": "채널에 추가하려는 구성원을 한 명 이상 선택해야 합니다.",
"mobile.client_upgrade": "애플리케이션 업데이트",
"mobile.client_upgrade.can_upgrade_subtitle": "새로운 버전을 다운로드할 수 있습니다.",

View File

@@ -198,6 +198,7 @@
"mobile.channel_list.members": "MEMBROS",
"mobile.channel_list.not_member": "NÃO É UM MEMBRO",
"mobile.channel_list.unreads": "NÃO LIDOS",
"mobile.channel_loader.still_loading": "Ainda tentando carregar seu conteúdo ...",
"mobile.channel_members.add_members_alert": "Você deve selecionar pelo menos um membro para adicionar ao canal.",
"mobile.client_upgrade": "Atualizar App",
"mobile.client_upgrade.can_upgrade_subtitle": "Uma nova versão está disponível para download.",

View File

@@ -198,6 +198,7 @@
"mobile.channel_list.members": "MEMBRI",
"mobile.channel_list.not_member": "NU E UN MEMBRU",
"mobile.channel_list.unreads": "NECITITE",
"mobile.channel_loader.still_loading": "Încă încerc să vă încărc conținutul...",
"mobile.channel_members.add_members_alert": "Trebuie să selectați cel puțin un membru pentru a adăuga la canal.",
"mobile.client_upgrade": "Actualizați aplicația",
"mobile.client_upgrade.can_upgrade_subtitle": "O versiune nouă este disponibilă pentru descărcare.",

View File

@@ -16,6 +16,7 @@
"center_panel.archived.closeChannel": "Закрити канал",
"channel.channelHasGuests": "This channel has guests",
"channel.hasGuests": "This group message has guests",
"channel.isGuest": "Ця людина гість",
"channel_header.addMembers": "Додати учасників",
"channel_header.directchannel.you": "{displayname} (ви)",
"channel_header.manageMembers": "Управління учасниками",
@@ -46,6 +47,8 @@
"channel_notifications.preference.global_default": "Глобальне налаштування за замовчуванням (Згадування)",
"channel_notifications.preference.header": "Надіслати сповіщення",
"channel_notifications.preference.never": "Ніколи",
"channel_notifications.preference.only_mentions": "Тільки згадки і особисті повідомлення",
"channel_notifications.preference.save_error": "Неможливо зберегти налаштування повідомлень. Будь ласка, перевірте ваше з'єднання і спробуйте ще раз.",
"combined_system_message.added_to_channel.many_expanded": "{users} і {finalUser} були **додані до каналу** {actor}.",
"combined_system_message.added_to_channel.one": "{firstUser} **запрошується на канал** користувачем {actor}. ",
"combined_system_message.added_to_channel.one_you": "Ви були **додані на канал** користувачем {actor}.",
@@ -81,12 +84,16 @@
"combined_system_message.you": "Ви",
"create_comment.addComment": "Додати коментар...",
"create_post.deactivated": "Ви переглядаєте архівований канал з деактивованим користувачем.",
"create_post.write": "Write to {channelDisplayName}",
"date_separator.today": "Today",
"date_separator.yesterday": "Yesterday",
"create_post.write": "Написати у {channelDisplayName}",
"date_separator.today": "Сьогодні",
"date_separator.yesterday": "Вчора",
"edit_post.editPost": "Редагувати повідомлення...",
"edit_post.save": "Зберегти",
"file_upload.fileAbove": "Не можна завантажити файл більше {max} МВ: {filename}",
"gallery.download_file": "Завантажити файл",
"gallery.footer.channel_name": "Поділилися в {channelName}",
"gallery.open_file": "Відкрити файл",
"gallery.unsuppored": "Попередній перегляд не підтримується для цього типу файлу",
"get_post_link_modal.title": "Скопіювати посилання",
"integrations.add": "Додати",
"intro_messages.anyMember": "Будь-який учасник може зайти і читати цей канал.",
@@ -157,6 +164,10 @@
"mobile.calendar.dayNamesShort": "Вс, Пн, Вт, Ср, Чт, Пт, Сб",
"mobile.calendar.monthNames": "Січень, лютий, березень, квітень, травень, червень, липень, серпень, вересень, жовтень, листопад, грудень",
"mobile.calendar.monthNamesShort": "Січень, лютий, березень, квітень, травень, червень, липень, серпень, середа, жовтень, листопад, грудень",
"mobile.camera_photo_permission_denied_description": "Робіть фотографії та завантажуйте їх у свій екземпляр Mattermost або зберігайте на своєму пристрої. Відкрийте \"Налаштування\", щоб надати камері доступ на читання та запис.",
"mobile.camera_photo_permission_denied_title": "{applicationName} хоче отримати доступ до вашої камери",
"mobile.camera_video_permission_denied_description": "Знімайте відео та завантажуйте їх у свій екземпляр Mattermost або зберігайте на своєму пристрої. Відкрийте \"Налаштування\", щоб надати камері доступ на читання та запис.",
"mobile.camera_video_permission_denied_title": "{applicationName} хоче отримати доступ до вашої камери",
"mobile.channel_drawer.search": "Перейти до ...",
"mobile.channel_info.alertMessageDeleteChannel": "Ви впевнені, що хочете архівувати {term} {name}?",
"mobile.channel_info.alertMessageLeaveChannel": "Ви дійсно бажаєте залишити {term} {name}?",
@@ -168,6 +179,8 @@
"mobile.channel_info.alertTitleUnarchiveChannel": "Архів {термін}",
"mobile.channel_info.alertYes": "Так",
"mobile.channel_info.convert": "Конвертувати в приватний канал",
"mobile.channel_info.convert_failed": "Не вдалося перетворити {displayName} на приватний канал.",
"mobile.channel_info.convert_success": "{displayName} тепер приватний канал.",
"mobile.channel_info.copy_header": "Копіювати заголовок",
"mobile.channel_info.copy_purpose": "Призначення копії",
"mobile.channel_info.delete_failed": "Не вдалося архівувати канал {displayName}. Перевірте ваше з'єднання та повторіть спробу.",
@@ -184,6 +197,7 @@
"mobile.channel_list.members": "УЧАСНИКИ",
"mobile.channel_list.not_member": "НЕ УЧАСНИК ",
"mobile.channel_list.unreads": "НЕ ПРОЧИТАНІ ",
"mobile.channel_loader.still_loading": "Все ще намагаюся завантажити вміст ...",
"mobile.channel_members.add_members_alert": "Ви повинні вибрати хоча б одного учасника для видалення з каналу. ",
"mobile.client_upgrade": "Оновити додаток",
"mobile.client_upgrade.can_upgrade_subtitle": "Нова версія доступна для завантаження.",
@@ -236,6 +250,7 @@
"mobile.emoji_picker.places": "МІСЦЯ",
"mobile.emoji_picker.recent": "НЕЩОДАВНІ ",
"mobile.emoji_picker.search.not_found_description": "Check the spelling or try another search.",
"mobile.emoji_picker.search.not_found_title": "Не знайдено результатів для \"{searchTerm}\"",
"mobile.emoji_picker.symbols": "СИМВОЛИ",
"mobile.error_handler.button": "Перезапустити",
"mobile.error_handler.description": "\nНатисніть на кнопку \"Запустити знову\", щоб відкрити програму заново. Після запуску, ви можете повідомити про проблему через меню налаштувань.\n",
@@ -249,6 +264,7 @@
"mobile.extension.title": "Поділіться в Mattermost",
"mobile.failed_network_action.retry": "Спробуй ще раз",
"mobile.failed_network_action.shortDescription": "Переконайтеся, що у вас активне з'єднання, і повторіть спробу.",
"mobile.failed_network_action.teams_channel_description": "Не вдалося завантажити канали для {teamName}.",
"mobile.failed_network_action.teams_description": "Не вдалося завантажити команди.",
"mobile.failed_network_action.teams_title": "Щось пішло не так",
"mobile.failed_network_action.title": "Немає підключення до Інтернету",
@@ -256,14 +272,17 @@
"mobile.file_upload.camera_photo": "Сфотографувати",
"mobile.file_upload.camera_video": "Візьміть відео",
"mobile.file_upload.disabled": "Завантаження файлів із мобільного пристрою вимкнено. Будь ласка, зверніться до системного адміністратора для отримання детальної інформації.",
"mobile.file_upload.disabled2": "Завантаження файлів із мобільного пристрою вимкнено.",
"mobile.file_upload.library": "Бібліотека фотозображень ",
"mobile.file_upload.max_warning": "Максимальна кількість завантажень - до 5 файлів.",
"mobile.file_upload.unsupportedMimeType": "Для зображення команди можна використовувати лише BMP, JPG або PNG ",
"mobile.file_upload.video": "Відео бібліотека",
"mobile.files_paste.error_description": "Помилка при направленні повідомлення. Будь-ласка, спробуйте знову.",
"mobile.files_paste.error_dismiss": "Відхилити",
"mobile.flagged_posts.empty_description": "Прапори - це спосіб позначення повідомлень для подальшого спостереження. Ваші прапори є особистими, і їх не можуть бачити інші користувачі.",
"mobile.flagged_posts.empty_title": "Немає позначених повідомлень",
"mobile.files_paste.error_title": "Не вдалося вставити",
"mobile.flagged_posts.empty_description": "Збережені повідомлення бачите лише ви. Позначте повідомлення для подальших дій або збережіть на потім, довго натискаючи повідомлення та вибираючи Зберегти в меню.",
"mobile.flagged_posts.empty_title": "Ще немає збережених повідомлень",
"mobile.gallery.title": "{index} з {total}",
"mobile.help.title": "Допомога ",
"mobile.intro_messages.DM": "Початок історії особистих повідомлень з {teammate}. Особисті повідомлення і файли доступні тут і не видно за межами цієї області.",
"mobile.intro_messages.default_message": "Це перший канал, який бачить новий учасник групи - використовуйте його для направлення повідомлень, які повинні побачити всі.",
@@ -280,6 +299,7 @@
"mobile.managed.blocked_by": "Заблоковано {vendor}",
"mobile.managed.exit": "Вхід",
"mobile.managed.jailbreak": "Пристрої з Jailbroken не є довіреними {vendor}, вийдіть з програми.",
"mobile.managed.not_secured.android": "Цей пристрій повинен бути захищений блокуванням екрана, щоб використовувати Mattermost.",
"mobile.managed.secured_by": "Захищено {vendor}",
"mobile.managed.settings": "Перейдіть до налаштувань",
"mobile.markdown.code.copy_code": "Копія коду",
@@ -341,9 +361,10 @@
"mobile.open_dm.error": "Ми не можемо підключитися до каналу {displayName}. Будь-ласка, перевірте підключення та спробуйте знову.",
"mobile.open_gm.error": "Помилка з'єднання із групи з цими користувачами. Будь-ласка, перевірте підключення та спробуйте заново.",
"mobile.open_unknown_channel.error": "Не вдається приєднатися до каналу. Скиньте кеш і спробуйте ще раз.",
"mobile.permission_denied_dismiss": "Не дозволяти",
"mobile.permission_denied_retry": "Налаштування",
"mobile.pinned_posts.empty_description": "Закріпіть важливі елементи, утримуючи на будь-якому повідомленні та обравши \"Прикріпити в каналі\".",
"mobile.pinned_posts.empty_title": "Немає закріплених повідомлень",
"mobile.pinned_posts.empty_description": "Закріпіть важливі повідомлення, видимі для всього каналу. Довго натискайте повідомлення та виберіть Закріпити на каналі, щоб зберегти його тут.",
"mobile.pinned_posts.empty_title": "Ще немає закріплених повідомлень",
"mobile.post.cancel": "Відміна",
"mobile.post.delete_question": "Ви впевнені, що хочете видалити це повідомлення?",
"mobile.post.delete_title": "Видалити публікацію",
@@ -353,30 +374,36 @@
"mobile.post.retry": "Оновити",
"mobile.post_info.add_reaction": "Додати реакцію ",
"mobile.post_info.copy_text": "Копіювати текст",
"mobile.post_info.flag": "Відмітити ",
"mobile.post_info.flag": "Зберегти",
"mobile.post_info.mark_unread": "Mark as Unread",
"mobile.post_info.pin": "Прикріпити в каналі",
"mobile.post_info.reply": "Відповідь",
"mobile.post_info.unflag": "Не позначено",
"mobile.post_info.unflag": "Вилучили зі збережених",
"mobile.post_info.unpin": "Від'єднати від каналу ",
"mobile.post_pre_header.flagged": "Позначено",
"mobile.post_pre_header.flagged": "Збережені",
"mobile.post_pre_header.pinned": "Прикріплено ",
"mobile.post_pre_header.pinned_flagged": "Прикріплений і позначений",
"mobile.post_pre_header.pinned_flagged": "Закріплено та збережено",
"mobile.post_textbox.entire_channel.cancel": "Відміна",
"mobile.post_textbox.entire_channel.confirm": "Підтвердьте",
"mobile.post_textbox.entire_channel.message": "Використовуючи @all або @channel, ви збираєтеся надсилати сповіщення** {totalMembers} людям** у **{timezones, number} {timezones, plural, one {timezone} other {timezones}}**. Ви впевнені, що хочете це зробити?",
"mobile.post_textbox.entire_channel.message.with_timezones": "Використовуючи @all або @channel, ви збираєтеся надсилати сповіщення** {totalMembers} людям** у **{timezones, number} {timezones, plural, one {timezone} other {timezones}}**. Ви впевнені, що хочете це зробити?",
"mobile.post_textbox.entire_channel.title": "Підтвердьте надсилання повідомлень на весь канал",
"mobile.post_textbox.groups.title": "Confirm sending notifications to groups",
"mobile.post_textbox.one_group.message.without_timezones": "Використовуючи {mention}, ви збираєтеся надіслати сповіщення {totalMembers} людям. Ви впевнені, що хочете це зробити?",
"mobile.post_textbox.uploadFailedDesc": "Деякі вкладення не змогли завантажити на сервер. Ви впевнені, що хочете опублікувати це повідомлення? ",
"mobile.post_textbox.uploadFailedTitle": "Вкладення несправності",
"mobile.posts_view.moreMsg": "Більше нових повідомлень ",
"mobile.prepare_file.failed_description": "Під час підготовки файлу сталася помилка. Будь ласка спробуйте ще раз.\n",
"mobile.prepare_file.failed_title": "Помилка підготовки",
"mobile.prepare_file.text": "Підготовка",
"mobile.privacy_link": "Privacy Policy",
"mobile.public_link.copied": "Публічне посилання скопійовано",
"mobile.push_notification_reply.button": "Відправити",
"mobile.push_notification_reply.placeholder": "Написати відповідь ...",
"mobile.push_notification_reply.title": "Відповідь",
"mobile.reaction_header.all_emojis": "All",
"mobile.recent_mentions.empty_description": "Тут відображатимуться повідомлення, що містять ваше ім'я користувача та інші слова, які викликають згадування.",
"mobile.recent_mentions.empty_title": "Немає останніх згадок",
"mobile.recent_mentions.empty_title": "Поки що згадок немає",
"mobile.rename_channel.display_name_maxLength": "Назва каналу має бути меншою за символи {maxLength, number}",
"mobile.rename_channel.display_name_minLength": "Назва каналу має містити {minLength, number} або більше символів",
"mobile.rename_channel.display_name_required": "Потрібна назва каналу ",
@@ -426,14 +453,18 @@
"mobile.search.recent_title": "Останні пошуки",
"mobile.select_team.join_open": "Інші команди, до яких ви можете приєднатися.",
"mobile.select_team.no_teams": "Немає доступних команд, до яких ви можете приєднатися.",
"mobile.server_link.error.text": "Посилання не вдалося знайти на цьому сервері.",
"mobile.server_link.error.title": "Link Error",
"mobile.server_link.unreachable_channel.error": "Постійне посилання належить до видаленого повідомлення або до каналу, на який ви не маєте доступу.",
"mobile.server_link.unreachable_team.error": "Постійне посилання належить до видаленого повідомлення або до каналу, на який ви не маєте доступу.",
"mobile.server_ssl.error.title": "Ненадійний сертифікат",
"mobile.server_upgrade.alert_description": "Ця версія сервера не підтримується, і користувачі будуть стикатися з проблемами сумісності, які спричиняють збої або серйозні помилки, що порушують основну функціональність програми. Потрібне оновлення до версії сервера {serverVersion} або пізнішої.",
"mobile.server_upgrade.button": "OK",
"mobile.server_upgrade.description": "\nДля продовження роботи потрібно оновлення сервера Mattermost. Будь-ласка, зверніться до адміністратора за подробицями.\n",
"mobile.server_upgrade.dismiss": "Відхилити",
"mobile.server_upgrade.learn_more": "Вчи більше",
"mobile.server_upgrade.title": "Потрібно оновлення сервера",
"mobile.server_url.empty": "Введіть дійсну URL-адресу сервера",
"mobile.server_url.invalid_format": "Адреса повинен починатися з http:// або https://",
"mobile.session_expired": "Сесія закінчилася. Увійдіть, щоб продовжувати отримувати сповіщення.",
"mobile.set_status.away": "Не на місці ",
@@ -446,10 +477,16 @@
"mobile.share_extension.error_message": "Під час використання розширення спільного використання сталася помилка.",
"mobile.share_extension.error_title": "Помилка розширення",
"mobile.share_extension.team": "Команди ",
"mobile.share_extension.too_long_message": "Кількість символів: {count}/{max}",
"mobile.share_extension.too_long_title": "Повідомлення задовге",
"mobile.sidebar_settings.permanent": "Постійна бічна панель",
"mobile.sidebar_settings.permanent_description": "Тримати бічну панель постійно відкритою",
"mobile.storage_permission_denied_description": "Завантажуйте файли у свій екземпляр Mattermost. Відкрийте \"Налаштування\", щоб надати доступ на читання та запис до файлів на цьому пристрої.",
"mobile.storage_permission_denied_title": "{applicationName} хоче отримати доступ до ваших файлів",
"mobile.suggestion.members": "Учасники ",
"mobile.system_message.channel_archived_message": "{username} архівує канал.",
"mobile.system_message.channel_unarchived_message": "{username} архівує канал.",
"mobile.system_message.update_channel_displayname_message_and_forget.updated_from": "{username} оновив відображуване ім’я каналу з: {oldDisplayName} на: {newDisplayName}",
"mobile.system_message.update_channel_header_message_and_forget.removed": "{Username} видалив заголовок каналу (було: {old})",
"mobile.system_message.update_channel_header_message_and_forget.updated_from": "{username} змінив(ла) заголовок каналу з {old} на {new}",
"mobile.system_message.update_channel_header_message_and_forget.updated_to": "{Username} змінив(ла) заголовок каналу на {new}",
@@ -465,9 +502,11 @@
"mobile.timezone_settings.select": "Виберіть часовий пояс",
"mobile.tos_link": "Умови обслуговування",
"mobile.unsupported_server.ok": "OK",
"mobile.unsupported_server.title": "Непідтримувана версія сервера",
"mobile.user.settings.notifications.email.fifteenMinutes": "Кожні 15 хвилин",
"mobile.user_list.deactivated": "Деактивувати",
"mobile.user_removed.message": "Вас видалили з каналу.",
"mobile.user_removed.title": "Видалено з {channelName}",
"mobile.video_playback.failed_description": "Під час спроби відтворити відео сталася помилка.\n",
"mobile.video_playback.failed_title": "Відтворення відео не вдалося",
"mobile.youtube_playback_error.description": "Під час спроби відтворити відео YouTube сталася помилка.\nПодробиці: {detail}",
@@ -487,7 +526,10 @@
"msg_typing.areTyping": "{users} і {last} друкують...",
"msg_typing.isTyping": "{користувач} друкує...",
"navbar.channel_drawer.button": "Канали та команди",
"navbar.channel_drawer.hint": "Відкриває панель каналів та команд",
"navbar.leave": "Залишити канал",
"navbar.more_options.button": "Більше параметрів",
"navbar.more_options.hint": "Відкриває інші параметри на правій бічній панелі",
"navbar.search.button": "Пошук каналу",
"navbar.search.hint": "Відкриває модальний пошук каналу",
"password_form.title": "Скидання пароля",
@@ -521,7 +563,7 @@
"search_bar.search": "Пошук",
"search_header.results": "Результати пошуку",
"search_header.title2": "Недавні згадки",
"search_header.title3": "Зазначені повідомлення ",
"search_header.title3": "Збережені повідомлення",
"search_item.channelArchived": "Архівувати ",
"sidebar.channels": "ПУБЛІЧНІ КАНАЛИ",
"sidebar.direct": "ПРЯМІ ПОСИЛАННЯ",
@@ -541,7 +583,7 @@
"suggestion.mention.all": "Повідомляє всіх у каналі",
"suggestion.mention.channel": "Повідомляє всіх у каналі",
"suggestion.mention.channels": "Мої канали",
"suggestion.mention.groups": "Group Mentions",
"suggestion.mention.groups": "Групові згадування",
"suggestion.mention.here": "Повідомляє всіх у каналі",
"suggestion.mention.members": "Учасник каналу",
"suggestion.mention.morechannels": "Інші канали",
@@ -559,6 +601,7 @@
"user.settings.display.normalClock": "12-годинний формат (приклад: 4:00 PM)",
"user.settings.display.preferTime": "Виберіть бажаний формат часу.",
"user.settings.general.email": "Електронна пошта ",
"user.settings.general.emailCantUpdate": "Електронну пошту потрібно оновлювати за допомогою веб-клієнта або настільної програми.",
"user.settings.general.emailGitlabCantUpdate": "Вхід здійснюється через GitLab. Електронна пошта не може бути оновлена. Електронна адреса, яку використовують для сповіщень, є {email}.",
"user.settings.general.emailGoogleCantUpdate": "Вхід здійснюється через Google. Електронна пошта не може бути оновлена. Електронна адреса, яку використовують для сповіщень, є {email}.",
"user.settings.general.emailHelp2": "Ваш системний адміністратор відключив електронну пошту. Ніякі повідомлення електронною поштою не надсилатимуться, доки їх не буде ввімкнено.",

View File

@@ -1,15 +1,15 @@
{
"about.date": "编译日期:",
"about.enterpriseEditionLearn": "了解更多关于企业版",
"about.enterpriseEditionLearn": "了解更多关于企业版 ",
"about.enterpriseEditionSt": "位于防火墙后的现代通讯方式。",
"about.enterpriseEditione1": "企业版",
"about.hash": "构建哈希:",
"about.hashee": "构建EE哈希:",
"about.teamEditionLearn": "加入 Mattermost 社区 ",
"about.hash": "编译哈希:",
"about.hashee": "企业版编译哈希:",
"about.teamEditionLearn": "加入 Mattermost 社区 ",
"about.teamEditionSt": "所有团队的通讯一站式解决,随时随地可搜索和访问。",
"about.teamEditiont0": "团队版",
"about.teamEditiont0": "团队版",
"about.teamEditiont1": "企业版",
"about.title": "关于 {appTitle}",
"about.title": "关于{appTitle}",
"announcment_banner.dont_show_again": "不再显示",
"api.channel.add_member.added": "{addedUsername} 被 {username} 添加到此频道.",
"archivedChannelMessage": "您正在查看**已归档的频道**。新消息无法被发送。",
@@ -89,7 +89,7 @@
"date_separator.yesterday": "昨天",
"edit_post.editPost": "编辑信息...",
"edit_post.save": "保存",
"file_upload.fileAbove": "文件超过{max}MB不能被上传{filename}",
"file_upload.fileAbove": "文件超过 {max}MB 不能被上传:{filename}",
"gallery.download_file": "下载文件",
"gallery.footer.channel_name": "在 {channelName} 中共享",
"gallery.open_file": "打开文件",
@@ -282,7 +282,7 @@
"mobile.files_paste.error_dismiss": "解除",
"mobile.files_paste.error_title": "粘贴失败",
"mobile.flagged_posts.empty_description": "只有您可见保存的消息。长按消息并在菜单选择保存可标记信息以便之后更进。",
"mobile.flagged_posts.empty_title": "无已标记的信息",
"mobile.flagged_posts.empty_title": "无保存的信息",
"mobile.gallery.title": "{index}/{total}",
"mobile.help.title": "帮助",
"mobile.intro_messages.DM": "这是您和{teammate}私信记录的开端。此区域外的人不能看到这里共享的私信和文件。",

View File

@@ -1,23 +1,23 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.2)
CFPropertyList (3.0.3)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
aws-eventstream (1.1.0)
aws-partitions (1.393.0)
aws-sdk-core (3.109.2)
aws-partitions (1.410.0)
aws-sdk-core (3.110.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.39.0)
aws-sdk-kms (1.40.0)
aws-sdk-core (~> 3, >= 3.109.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.84.1)
aws-sdk-s3 (1.86.2)
aws-sdk-core (~> 3, >= 3.109.0)
aws-sdk-kms (~> 1)
aws-sdk-kms (~> 1.26)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.2)
aws-eventstream (~> 1, >= 1.0.2)
@@ -29,13 +29,13 @@ GEM
highline (~> 1.7.2)
declarative (0.0.20)
declarative-option (0.1.0)
digest-crc (0.6.1)
rake (~> 13.0)
digest-crc (0.6.2)
rake (~> 12.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
emoji_regex (3.2.1)
excon (0.78.0)
excon (0.78.1)
faraday (1.1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords
@@ -45,7 +45,7 @@ GEM
faraday_middleware (1.0.0)
faraday (~> 1.0)
fastimage (2.2.0)
fastlane (2.167.0)
fastlane (2.170.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
aws-sdk-s3 (~> 1.0)
@@ -101,7 +101,7 @@ GEM
google-cloud-env (1.4.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.0.1)
google-cloud-storage (1.29.1)
google-cloud-storage (1.29.2)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
@@ -120,7 +120,7 @@ GEM
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.4.0)
json (2.3.1)
json (2.4.1)
jwt (2.2.2)
memoist (0.16.2)
mini_magick (4.11.0)
@@ -135,7 +135,7 @@ GEM
os (1.1.1)
plist (3.5.0)
public_suffix (4.0.6)
rake (13.0.1)
rake (12.3.3)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)

View File

@@ -911,7 +911,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 337;
CURRENT_PROJECT_VERSION = 341;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
@@ -953,7 +953,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 337;
CURRENT_PROJECT_VERSION = 341;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;

View File

@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.38.0</string>
<string>1.39.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -36,7 +36,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>337</string>
<string>341</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
@@ -129,10 +129,5 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
</array>
</dict>
</plist>

View File

@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.38.0</string>
<string>1.39.0</string>
<key>CFBundleVersion</key>
<string>337</string>
<string>341</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>

View File

@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.38.0</string>
<string>1.39.0</string>
<key>CFBundleVersion</key>
<string>337</string>
<string>341</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View File

@@ -359,7 +359,7 @@ PODS:
- React
- RNVectorIcons (7.1.0):
- React
- Rudder (1.0.9)
- Rudder (1.0.10)
- SDWebImage (5.9.4):
- SDWebImage/Core (= 5.9.4)
- SDWebImage/Core (5.9.4)
@@ -672,7 +672,7 @@ SPEC CHECKSUMS:
RNShare: 106a76243ac90f43ddb9028dcb78ade406b8adff
RNSVG: ce9d996113475209013317e48b05c21ee988d42e
RNVectorIcons: bc69e6a278b14842063605de32bec61f0b251a59
Rudder: 90ed801a09c73017184e6fc901370be1754eb182
Rudder: 05e61fe2e59bcd65931f4a47d21011e15adf7159
SDWebImage: b69257f4ab14e9b6a2ef53e910fdf914d8f757c1
SDWebImageWebPCoder: d0dac55073088d24b2ac1b191a71a8f8d0adac21
Sentry: e2c691627ae1dc0029acebbd1b0b4af4df12af73

8
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "mattermost-mobile",
"version": "1.38.0",
"version": "1.39.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -6130,9 +6130,9 @@
}
},
"@rudderstack/rudder-sdk-react-native": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rudderstack/rudder-sdk-react-native/-/rudder-sdk-react-native-1.0.3.tgz",
"integrity": "sha512-eYvNPh+x/XxcxM+jsUO5MN1y0U+PwIiHZyzvM66eIx/fwcPLQDLNDqW3pj99dhtf/9sv5Gu4EXtT3hFtLJ76pg==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@rudderstack/rudder-sdk-react-native/-/rudder-sdk-react-native-1.0.4.tgz",
"integrity": "sha512-jSzgtHvuv0gqFgsCAsyyV4Cmk+rWg8TgPiLHLay8xELLxvIwRalkBv5bN3eFRJOYJQuIwR1KBQ3+p7nFHszN2w==",
"requires": {
"@babel/runtime": "^7.7.7",
"@types/react-native": "^0.62.2"

View File

@@ -1,6 +1,6 @@
{
"name": "mattermost-mobile",
"version": "1.38.0",
"version": "1.39.0",
"description": "Mattermost Mobile with React Native",
"repository": "git@github.com:mattermost/mattermost-mobile.git",
"author": "Mattermost, Inc.",
@@ -15,7 +15,7 @@
"@react-native-community/netinfo": "5.9.7",
"@react-navigation/native": "5.8.9",
"@react-navigation/stack": "5.12.6",
"@rudderstack/rudder-sdk-react-native": "1.0.3",
"@rudderstack/rudder-sdk-react-native": "1.0.4",
"@sentry/react-native": "2.0.0",
"analytics-react-native": "1.2.0",
"commonmark": "github:mattermost/commonmark.js#f6ab98dede6ce4b4e7adea140ac77249bfb2d6ce",

View File

@@ -1,5 +1,5 @@
diff --git a/node_modules/react-native-keyboard-tracking-view/lib/KeyboardTrackingViewManager.m b/node_modules/react-native-keyboard-tracking-view/lib/KeyboardTrackingViewManager.m
index 1333a10..53b73a6 100644
index 1333a10..6922a17 100644
--- a/node_modules/react-native-keyboard-tracking-view/lib/KeyboardTrackingViewManager.m
+++ b/node_modules/react-native-keyboard-tracking-view/lib/KeyboardTrackingViewManager.m
@@ -23,7 +23,7 @@
@@ -197,7 +197,7 @@ index 1333a10..53b73a6 100644
}
}
else if(self.scrollBehavior == KeyboardTrackingScrollBehaviorFixedOffset && !self.isDraggingScrollView)
@@ -422,16 +459,20 @@ - (void)_updateScrollViewInsets
@@ -422,16 +459,21 @@ - (void)_updateScrollViewInsets
self.scrollViewToManage.contentOffset = CGPointMake(originalOffset.x, originalOffset.y + insetsDiff);
}
@@ -219,14 +219,15 @@ index 1333a10..53b73a6 100644
+ self.scrollViewToManage.frame = frame;
+
+ if (self.accessoriesContainer) {
+ self.accessoriesContainer.bounds = CGRectMake(self.accessoriesContainer.bounds.origin.x, positionY,
+ CGFloat containerPositionY = self.normalList ? 0 : _observingInputAccessoryView.keyboardHeight;
+ self.accessoriesContainer.bounds = CGRectMake(self.accessoriesContainer.bounds.origin.x, containerPositionY,
+ self.accessoriesContainer.bounds.size.width, self.accessoriesContainer.bounds.size.height);
}
- self.scrollViewToManage.scrollIndicatorInsets = insets;
}
}
@@ -448,7 +489,6 @@ -(void)addBottomViewIfNecessary
@@ -448,7 +490,6 @@ -(void)addBottomViewIfNecessary
if (self.addBottomView && _bottomView == nil)
{
_bottomView = [UIView new];
@@ -234,7 +235,7 @@ index 1333a10..53b73a6 100644
[self addSubview:_bottomView];
[self updateBottomViewFrame];
}
@@ -467,6 +507,12 @@ -(void)updateBottomViewFrame
@@ -467,6 +508,12 @@ -(void)updateBottomViewFrame
}
}
@@ -247,7 +248,7 @@ index 1333a10..53b73a6 100644
#pragma mark - safe area
-(void)safeAreaInsetsDidChange
@@ -510,7 +556,7 @@ -(void)updateTransformAndInsets
@@ -510,7 +557,7 @@ -(void)updateTransformAndInsets
CGFloat accessoryTranslation = MIN(-bottomSafeArea, -_observingInputAccessoryView.keyboardHeight);
if (_observingInputAccessoryView.keyboardHeight <= bottomSafeArea) {
@@ -256,7 +257,7 @@ index 1333a10..53b73a6 100644
} else if (_observingInputAccessoryView.keyboardState != KeyboardStateWillHide) {
_bottomViewHeight = 0;
}
@@ -582,6 +628,8 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView
@@ -582,6 +629,8 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
self.isDraggingScrollView = YES;
@@ -265,7 +266,7 @@ index 1333a10..53b73a6 100644
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
@@ -592,6 +640,15 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoi
@@ -592,6 +641,15 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoi
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
self.isDraggingScrollView = NO;
@@ -281,7 +282,7 @@ index 1333a10..53b73a6 100644
}
- (CGFloat)getKeyboardHeight
@@ -634,6 +691,12 @@ @implementation KeyboardTrackingViewManager
@@ -634,6 +692,12 @@ @implementation KeyboardTrackingViewManager
RCT_REMAP_VIEW_PROPERTY(addBottomView, addBottomView, BOOL)
RCT_REMAP_VIEW_PROPERTY(scrollToFocusedInput, scrollToFocusedInput, BOOL)
RCT_REMAP_VIEW_PROPERTY(allowHitsOutsideBounds, allowHitsOutsideBounds, BOOL)
@@ -294,7 +295,7 @@ index 1333a10..53b73a6 100644
+ (BOOL)requiresMainQueueSetup
{
@@ -654,6 +717,20 @@ - (UIView *)view
@@ -654,6 +718,20 @@ - (UIView *)view
return [[KeyboardTrackingView alloc] init];
}

View File

@@ -30,6 +30,7 @@ jest.doMock('react-native', () => {
Alert: RNAlert,
InteractionManager: RNInteractionManager,
NativeModules: RNNativeModules,
Linking: RNLinking,
} = ReactNative;
const Alert = {
@@ -96,6 +97,11 @@ jest.doMock('react-native', () => {
},
};
const Linking = {
...RNLinking,
openURL: jest.fn(),
};
return Object.setPrototypeOf({
Platform: {
...Platform,
@@ -110,6 +116,7 @@ jest.doMock('react-native', () => {
Alert,
InteractionManager,
NativeModules,
Linking,
}, ReactNative);
});
@@ -187,7 +194,6 @@ jest.mock('react-native-cookies', () => ({
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
openURL: jest.fn(),
canOpenURL: jest.fn(),
getInitialURL: jest.fn(),
clearAll: jest.fn(),
get: () => Promise.resolve(({