forked from Ivasoft/mattermost-mobile
Compare commits
17 Commits
v1
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15e081f572 | ||
|
|
1be535cc22 | ||
|
|
6a2f02be62 | ||
|
|
4f28e61632 | ||
|
|
c7cf32ebb8 | ||
|
|
142a04fac5 | ||
|
|
60b82ea5a8 | ||
|
|
64989a728c | ||
|
|
7317ffeb21 | ||
|
|
49031c26d4 | ||
|
|
c133dab50f | ||
|
|
47c0ff2655 | ||
|
|
55fc50d7c2 | ||
|
|
8b0c831814 | ||
|
|
971d5990e8 | ||
|
|
e91af36780 | ||
|
|
70e5cace11 |
@@ -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'
|
||||
|
||||
@@ -15,7 +15,7 @@ import Store from '@store/store';
|
||||
|
||||
Navigation.setDefaultOptions({
|
||||
layout: {
|
||||
orientation: [DeviceTypes.IS_TABLET ? undefined : 'portrait'],
|
||||
orientation: [DeviceTypes.IS_TABLET ? 'all' : 'portrait'],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ export function showPermalink(intl: typeof intlShape, teamName: string, postId:
|
||||
showModalOverCurrentContext(screen, passProps, options);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,4 @@ function mapStateToProps(state) {
|
||||
};
|
||||
}
|
||||
|
||||
export const AUTOCOMPLETE_MAX_HEIGHT = 200;
|
||||
|
||||
export default connect(mapStateToProps, null, null, {forwardRef: true})(Autocomplete);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ describe('DraftInput', () => {
|
||||
},
|
||||
membersCount: 10,
|
||||
addRecentUsedEmojisInMessage: jest.fn(),
|
||||
handleGotoLocation: jest.fn(),
|
||||
};
|
||||
const ref = React.createRef();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
export default {
|
||||
CHANNEL: 'channel',
|
||||
DMCHANNEL: 'dmchannel',
|
||||
GROUPCHANNEL: 'groupchannel',
|
||||
PERMALINK: 'permalink',
|
||||
OTHER: 'other',
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "新しいバージョンが利用できます。",
|
||||
|
||||
@@ -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": "새로운 버전을 다운로드할 수 있습니다.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "Ваш системний адміністратор відключив електронну пошту. Ніякі повідомлення електронною поштою не надсилатимуться, доки їх не буде ввімкнено.",
|
||||
|
||||
@@ -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}私信记录的开端。此区域外的人不能看到这里共享的私信和文件。",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
8
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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(({
|
||||
|
||||
Reference in New Issue
Block a user