Compare commits

...

42 Commits

Author SHA1 Message Date
Dean Whillier
aa5ecaf417 Bump app build number to 234 (#3327)
# Conflicts:
#	android/app/build.gradle
#	ios/Mattermost.xcodeproj/project.pbxproj
#	ios/Mattermost/Info.plist
#	ios/MattermostShare/Info.plist
#	ios/MattermostTests/Info.plist
#	ios/NotificationService/Info.plist
2019-09-25 16:15:06 -04:00
Dean Whillier
65cad4c562 Bump app version number to 1.23.1 (#3326) 2019-09-25 16:05:58 -04:00
Miguel Alatzar
290e65ac57 Various fixes (#3287)
* Ensure onAppStateChange runs only after GlobalEventHandler is configured (#3268)

* Updated Info.plist with new bluetooth usage description key (#3275)

* Call scrollToIndex only when ref is set and index is in range (#3285)

* Update ios/Mattermost/Info.plist

Co-Authored-By: Elias Nahum <nahumhbl@gmail.com>

* Null check on current (#3309)

* Null check on current

* Null check on current

* [MM-18779] Reset moment local on logout (#3310)

* Reset moment local on logout

* Update app/selectors/i18n.js

Co-Authored-By: Elias Nahum <nahumhbl@gmail.com>
2019-09-25 11:52:07 -07:00
Mattermost Build
20a2078377 add circleci (#3315) 2019-09-24 17:28:38 -07:00
Devin Binnie
8ace8c6212 Bump app build number to 230 (#3256) 2019-09-12 15:09:17 -04:00
Mattermost Build
5b858e2f06 Automated cherry pick of #3238 (#3255)
* Set moment timezone locale as needed to prevent moment and intl locale getting out of sync

* Update app/i18n/index.js

Co-Authored-By: Elias Nahum <nahumhbl@gmail.com>

* Updates per feedback
2019-09-12 11:57:34 -03:00
Mattermost Build
a9064aca0f When searching for profiles, allow inactive (#3254) 2019-09-12 11:31:58 -03:00
Mattermost Build
521435748e Automated cherry pick of #3240 (#3253)
* Fix edit profile screen being dismissed when removing profile picture

* Fix test
2019-09-12 11:30:58 -03:00
Mattermost Build
001c105ff7 Automated cherry pick of #3242 (#3247)
* MM-18465 Add proper flags when opening settings

* Fix undefined setPerformingAuthentication Function in emm_provider.
2019-09-12 00:59:44 -03:00
Elias Nahum
603caaf125 translations PR 20190910 (#3241) 2019-09-11 18:41:46 -03:00
Mattermost Build
069774231c Automated cherry pick of #3233 (#3237)
* Update webview library to remove deprecated API

* Remove unnecessary timeout
2019-09-10 18:32:06 -03:00
Mattermost Build
be5ff217da Replicated missing user functionality for system messages from webapp in RN (#3236) 2019-09-10 13:08:39 -04:00
Mattermost Build
8004a906cf Removing caching of failed image loading attempts (#3234) 2019-09-10 13:04:17 -04:00
Devin Binnie
46581bb6d2 Bump app build number to 229 (#3232) 2019-09-09 13:29:15 -04:00
Elias Nahum
9f92824dae MM-18223 Fix channel actions on combined sidebar (#3221) 2019-09-09 21:25:40 +08:00
Devin Binnie
f530c227b2 [MM-18172] Added state for message length such that alerts for long length aren't fired too often - cherry-pick (#3225)
* [MM-18172] Added state for message length such that alerts for long length aren't fired too often (#3218)

* [MM-18172] Added state for message length such that alerts for long length aren't fired too often

* Added a test

* Naming change

* Merge'd

* Merge'd
2019-09-05 09:39:39 -04:00
Elias Nahum
90f6fd6cb8 MM-18179 Clear cache and cookies for SSO login (#3220)
* MM-18179 Clear cache and cookies for SSO login

* Update app/components/failed_network_action/index.js

Co-Authored-By: Miguel Alatzar <migbot@users.noreply.github.com>
2019-09-05 20:11:07 +09:00
Devin Binnie
b37811d8e1 Bump app build number to 228 (#3215) 2019-09-03 15:47:42 -04:00
Miguel Alatzar
dc81742404 Rebuild emojis when emojis change and use props.deviceWidth (#3208) 2019-09-01 10:59:44 +09:00
Elias Nahum
f005a60a8c translations PR 20190830 (#3205) 2019-08-31 07:04:53 +09:00
Mattermost Build
9d357a0333 Increase cancel button width (#3204) 2019-08-30 05:31:02 +05:30
Elias Nahum
e100ffc7dd translations PR 20190826 (#3197) 2019-08-29 17:48:58 -04:00
Devin Binnie
94995eb0b9 Bump app build number to 227 (#3200) 2019-08-28 09:59:46 -04:00
Mattermost Build
0b49403e92 Automated cherry pick of #3195 (#3196)
* Bump app build number to 226

* Bump app version number to 1.23.0
2019-08-26 10:59:39 -04:00
Miguel Alatzar
4dfecea7d1 Set rebuilt emojis only after search bar animation completes (#3191) 2019-08-26 10:42:00 -04:00
Mattermost Build
ada8d4863b Re-render MainSidebar on theme change (#3194) 2019-08-26 10:19:17 -04:00
Mattermost Build
ac2e8dc02f Automated cherry pick of #3170 (#3193)
* update attachment image when image url changes

* update image dimensions from metadata

* use componentDidUpdate

* add tests for AttachmentImage

* add test for when imageUrl is not present
2019-08-26 10:18:57 -04:00
Mattermost Build
3b6ca6fdb8 MM-17792 Do not show download option for files uploaded locally (#3192) 2019-08-26 10:17:54 -04:00
Mattermost Build
26c4777365 Bump app build number to 225 (#3188) 2019-08-23 22:23:35 -04:00
Mattermost Build
820b3f73e6 do not dispatch navigation actions when opening the app from a push notification (#3185) 2019-08-23 22:20:37 -04:00
Mattermost Build
344de147bd Bump app build number to 224 (#3182) 2019-08-23 16:05:37 -04:00
Mattermost Build
92cfb3ddef Update sentry (#3180) 2019-08-23 16:01:43 -04:00
Mattermost Build
8fd6c86705 Automated cherry pick of #3137 (#3176)
* MM-17589 Update passcode required help text in share extension

* MM-17589 Update passcode required help text in app
2019-08-23 12:27:58 -04:00
Mattermost Build
8f7d852cb1 Bump app build number to 223 (#3174) 2019-08-23 10:14:12 -04:00
Mattermost Build
b36b395a0b MM-18024 MM-18025 Fix race condition when push notification opens the app and credentials are not set (#3171) 2019-08-23 10:04:24 -04:00
Mattermost Build
0115610152 MM-17886 Do not wait to cancel the video download (#3166) 2019-08-22 20:55:11 -04:00
Mattermost Build
db144dc5a7 Automated cherry pick of #3160 (#3161)
* Bump app build number to 222

* Bump app version number to 1.22.1
2019-08-21 19:14:21 -04:00
Mattermost Build
3427e917f4 update moment locale as lowercase (#3157) 2019-08-21 19:07:06 -04:00
Mattermost Build
b8844c13b7 Automated cherry pick of #3152 (#3156)
* Check for valid server URL

* Fallback to parsing token
2019-08-21 16:06:29 -07:00
Elias Nahum
0bc2d43448 translations PR 20190819 (#3150) 2019-08-20 09:47:53 +02:00
Mattermost Build
f79c33099e Automated cherry pick of #3127 (#3151)
* MM-17700 - Updating bot tag css

* Updating spacing for android
2019-08-19 17:18:26 -07:00
Mattermost Build
57d148bbf9 Android screen right button colors (#3148) 2019-08-19 10:33:47 -07:00
73 changed files with 1147 additions and 335 deletions

23
.circleci/config.yml Normal file
View File

@@ -0,0 +1,23 @@
version: 2.1
jobs:
test:
working_directory: ~/mattermost-mobile
docker:
- image: circleci/node:10
steps:
- checkout
- run: |
echo assets/base/config.json
cat assets/base/config.json
# Avoid installing pods
touch .podinstall
# Run tests
make test || exit 1
workflows:
version: 2
pr-test:
jobs:
- test

View File

@@ -123,8 +123,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
missingDimensionStrategy "RNN.reactNativeVersion", "reactNative57_5"
versionCode 221
versionName "1.22.0"
versionName "1.23.1"
versionCode 234
multiDexEnabled = true
ndk {
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'

View File

@@ -5,6 +5,7 @@ import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.provider.Settings;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.NativeModule;
@@ -69,7 +70,10 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
@ReactMethod
// Close the current activity and open the security settings.
public void goToSecuritySettings() {
getReactApplicationContext().startActivity(new Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS));
Intent intent = new Intent(Settings.ACTION_SECURITY_SETTINGS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getReactApplicationContext().startActivity(intent);
getCurrentActivity().finish();
System.exit(0);
}

View File

@@ -11,6 +11,7 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.HttpUrl;
import org.json.JSONObject;
import org.json.JSONException;
@@ -37,6 +38,13 @@ public class ReceiptDelivery {
if (map != null) {
String token = map.getString("password");
String serverUrl = map.getString("service");
if (serverUrl.isEmpty()) {
String[] credentials = token.split(",[ ]*");
if (credentials.length == 2) {
token = credentials[0];
serverUrl = credentials[1];
}
}
Log.i("ReactNative", String.format("Send receipt delivery ACK=%s TYPE=%s to URL=%s with TOKEN=%s", ackId, type, serverUrl, token));
execute(serverUrl, token, ackId, type);
@@ -64,21 +72,24 @@ public class ReceiptDelivery {
return;
}
final OkHttpClient client = new OkHttpClient();
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
RequestBody body = RequestBody.create(JSON, json.toString());
Request request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.header("Content-Type", "application/json")
.url(String.format("%s/api/v4/notifications/ack", serverUrl.replaceAll("/$", "")))
.post(body)
.build();
final HttpUrl url = HttpUrl.parse(
String.format("%s/api/v4/notifications/ack", serverUrl.replaceAll("/$", "")));
if (url != null) {
final OkHttpClient client = new OkHttpClient();
final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
RequestBody body = RequestBody.create(JSON, json.toString());
Request request = new Request.Builder()
.header("Authorization", String.format("Bearer %s", token))
.header("Content-Type", "application/json")
.url(url)
.post(body)
.build();
try {
client.newCall(request).execute();
} catch (Exception e) {
Log.e("ReactNative", "Receipt delivery failed to send");
try {
client.newCall(request).execute();
} catch (Exception e) {
Log.e("ReactNative", "Receipt delivery failed to send");
}
}
}
}

View File

@@ -3,6 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Platform, View} from 'react-native';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import FormattedText from 'app/components/formatted_text';
@@ -23,11 +24,13 @@ export default class BotTag extends PureComponent {
const style = createStyleSheet(this.props.theme);
return (
<FormattedText
id='post_info.bot'
defaultMessage='BOT'
style={style.bot}
/>
<View style={style.bot}>
<FormattedText
id='post_info.bot'
defaultMessage='BOT'
style={style.botText}
/>
</View>
);
}
}
@@ -39,12 +42,20 @@ const createStyleSheet = makeStyleSheetFromTheme((theme) => {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.15),
borderRadius: 2,
color: theme.centerChannelColor,
fontSize: 10,
fontWeight: '600',
marginRight: 5,
marginLeft: 5,
marginRight: 2,
marginBottom: 1,
...Platform.select({
android: {
marginBottom: 0,
},
}),
marginLeft: 2,
paddingVertical: 2,
paddingHorizontal: 4,
},
botText: {
fontSize: 10,
},
};
});

View File

@@ -241,12 +241,16 @@ export default class CombinedSystemMessage extends React.PureComponent {
getUsernamesByIds = (userIds = []) => {
const {currentUserId, currentUsername} = this.props;
const allUsernames = this.getAllUsernames();
const {formatMessage} = this.context.intl;
const someone = formatMessage({id: t('channel_loader.someone'), defaultMessage: 'Someone'});
const usernames = userIds.
filter((userId) => {
return userId !== currentUserId && userId !== currentUsername;
}).
map((userId) => {
return `@${allUsernames[userId]}`;
return allUsernames[userId] ? `@${allUsernames[userId]}` : someone;
}).filter((username) => {
return username && username !== '';
});

View File

@@ -0,0 +1,129 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/emoji_picker/EmojiPicker should match snapshot 1`] = `
<Connect(SafeAreaIos)
excludeHeader={true}
>
<KeyboardAvoidingView
behavior="padding"
enabled={true}
keyboardVerticalOffset={107}
style={
Object {
"flex": 1,
}
}
>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.2)",
"paddingVertical": 5,
}
}
>
<SearchBarIos
autoCapitalize="none"
backgroundColor="transparent"
blurOnSubmit={true}
cancelTitle="Cancel"
inputHeight={33}
inputStyle={
Object {
"backgroundColor": "#ffffff",
"color": "#3d3c40",
"fontSize": 13,
}
}
leftComponent={null}
onAnimationComplete={[Function]}
onBlur={[Function]}
onCancelButtonPress={[Function]}
onChangeText={[Function]}
onFocus={[Function]}
onSearchButtonPress={[Function]}
onSelectionChange={[Function]}
placeholder="Search"
placeholderTextColor="rgba(61,60,64,0.5)"
searchIconCollapsedMargin={10}
searchIconExpandedMargin={10}
tintColorDelete="rgba(61,60,64,0.5)"
tintColorSearch="rgba(61,60,64,0.8)"
titleCancelColor="#3d3c40"
value=""
/>
</View>
<View
style={
Object {
"alignItems": "center",
"backgroundColor": "#ffffff",
"flex": 1,
}
}
>
<SectionList
ListFooterComponent={[Function]}
data={Array []}
disableVirtualization={false}
getItemLayout={[Function]}
horizontal={false}
initialNumToRender={10}
keyExtractor={[Function]}
keyboardShouldPersistTaps="always"
maxToRenderPerBatch={10}
onEndReached={[Function]}
onEndReachedThreshold={0}
onMomentumScrollEnd={[Function]}
onScroll={[Function]}
onScrollToIndexFailed={[Function]}
pageSize={30}
removeClippedSubviews={true}
renderItem={[Function]}
renderSectionHeader={[Function]}
scrollEventThrottle={50}
sections={Array []}
showsVerticalScrollIndicator={false}
stickySectionHeadersEnabled={true}
style={
Array [
Object {
"backgroundColor": "#ffffff",
"marginBottom": 35,
},
Object {
"width": 370,
},
]
}
updateCellsBatchingPeriod={50}
windowSize={21}
/>
<View
style={
Object {
"bottom": 0,
"height": 35,
"left": 0,
"position": "absolute",
"right": 0,
"width": "100%",
}
}
>
<View
style={
Object {
"backgroundColor": "rgba(61,60,64,0.1)",
"borderTopColor": "rgba(61,60,64,0.3)",
"borderTopWidth": 1,
"flexDirection": "row",
"justifyContent": "space-between",
}
}
/>
</View>
</View>
</KeyboardAvoidingView>
</Connect(SafeAreaIos)>
`;

View File

@@ -35,6 +35,10 @@ const SECTION_MARGIN = 15;
const SECTION_HEADER_HEIGHT = 28;
const EMOJIS_PER_PAGE = 200;
export function filterEmojiSearchInput(searchText) {
return searchText.toLowerCase().replace(/^:|:$/g, '');
}
export default class EmojiPicker extends PureComponent {
static propTypes = {
customEmojisEnabled: PropTypes.bool.isRequired,
@@ -87,9 +91,9 @@ export default class EmojiPicker extends PureComponent {
}
componentWillReceiveProps(nextProps) {
let rebuildEmojis = false;
this.rebuildEmojis = false;
if (this.props.deviceWidth !== nextProps.deviceWidth) {
rebuildEmojis = true;
this.rebuildEmojis = true;
if (this.refs.search_bar) {
this.refs.search_bar.blur();
@@ -97,14 +101,16 @@ export default class EmojiPicker extends PureComponent {
}
if (this.props.emojis !== nextProps.emojis) {
rebuildEmojis = true;
this.rebuildEmojis = true;
this.setRebuiltEmojis();
}
}
if (rebuildEmojis) {
const emojis = this.renderableEmojis(this.props.emojisBySection, nextProps.deviceWidth);
this.setState({
emojis,
});
setRebuiltEmojis = (searchBarAnimationComplete = true) => {
if (this.rebuildEmojis && searchBarAnimationComplete) {
this.rebuildEmojis = false;
const emojis = this.renderableEmojis(this.props.emojisBySection, this.props.deviceWidth);
this.setState({emojis});
}
}
@@ -160,24 +166,25 @@ export default class EmojiPicker extends PureComponent {
});
};
changeSearchTerm = (text) => {
changeSearchTerm = (rawText) => {
const searchTerm = filterEmojiSearchInput(rawText);
const nextState = {
searchTerm: text,
searchTerm: rawText,
};
if (!text) {
nextState.currentSectionIndex = 0;
}
this.setState(nextState);
if (!searchTerm) {
nextState.currentSectionIndex = 0;
return;
}
clearTimeout(this.searchTermTimeout);
const timeout = text ? 100 : 0;
const timeout = searchTerm ? 100 : 0;
this.searchTermTimeout = setTimeout(async () => {
if (isMinimumServerVersion(this.props.serverVersion, 4, 7)) {
await this.props.actions.searchCustomEmojis(text);
await this.props.actions.searchCustomEmojis(searchTerm);
}
const filteredEmojis = this.searchEmojis(text);
const filteredEmojis = this.searchEmojis(searchTerm);
this.setState({
filteredEmojis,
});
@@ -476,6 +483,7 @@ export default class EmojiPicker extends PureComponent {
onCancelButtonPress={this.cancelSearch}
autoCapitalize='none'
value={searchTerm}
onAnimationComplete={this.setRebuiltEmojis}
/>
</View>
<View style={styles.container}>

View File

@@ -0,0 +1,115 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import Preferences from 'mattermost-redux/constants/preferences';
import {shallowWithIntl} from 'test/intl-test-helper';
import EmojiPicker, {filterEmojiSearchInput} from './emoji_picker.js';
describe('components/emoji_picker/EmojiPicker', () => {
const baseProps = {
actions: {
getCustomEmojis: jest.fn(),
incrementEmojiPickerPage: jest.fn(),
searchCustomEmojis: jest.fn(),
},
customEmojisEnabled: false,
customEmojiPage: 200,
deviceWidth: 400,
emojis: [],
emojisBySection: [],
fuse: {},
isLandscape: false,
theme: Preferences.THEMES.default,
};
const testCases = [
{input: 'smile', output: 'smile'},
{input: 'SMILE', output: 'smile'},
{input: ':smile', output: 'smile'},
{input: ':SMILE', output: 'smile'},
{input: 'smile:', output: 'smile'},
{input: 'SMILE:', output: 'smile'},
{input: ':smile:', output: 'smile'},
{input: ':SMILE:', output: 'smile'},
];
testCases.forEach((testCase) => {
test(`'${testCase.input}' should return '${testCase.output}'`, () => {
expect(filterEmojiSearchInput(testCase.input)).toEqual(testCase.output);
});
});
test('should match snapshot', () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should set rebuildEmojis to true when deviceWidth changes', () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const instance = wrapper.instance();
expect(instance.rebuildEmojis).toBe(undefined);
const newDeviceWidth = baseProps.deviceWidth * 2;
wrapper.setProps({deviceWidth: newDeviceWidth});
expect(instance.rebuildEmojis).toBe(true);
});
test('should rebuild emojis emojis when emojis change', () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const instance = wrapper.instance();
const renderableEmojis = jest.spyOn(instance, 'renderableEmojis');
expect(instance.rebuildEmojis).toBe(undefined);
const newEmojis = [{}];
wrapper.setProps({emojis: newEmojis});
expect(renderableEmojis).toHaveBeenCalledWith(baseProps.emojisBySection, baseProps.deviceWidth);
});
test('should set rebuilt emojis when rebuildEmojis is true and searchBarAnimationComplete is true', () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const instance = wrapper.instance();
instance.setState = jest.fn();
instance.renderableEmojis = jest.spyOn(instance, 'renderableEmojis');
instance.rebuildEmojis = true;
const searchBarAnimationComplete = true;
const setRebuiltEmojis = jest.spyOn(instance, 'setRebuiltEmojis');
setRebuiltEmojis(searchBarAnimationComplete);
expect(instance.setState).toHaveBeenCalledWith({emojis: []});
expect(instance.rebuildEmojis).toBe(false);
});
test('should not set rebuilt emojis when rebuildEmojis is false and searchBarAnimationComplete is true', () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const instance = wrapper.instance();
instance.setState = jest.fn();
instance.rebuildEmojis = false;
const searchBarAnimationComplete = true;
const setRebuiltEmojis = jest.spyOn(instance, 'setRebuiltEmojis');
setRebuiltEmojis(searchBarAnimationComplete);
expect(instance.setState).not.toHaveBeenCalled();
});
test('should not set rebuilt emojis when rebuildEmojis is true and searchBarAnimationComplete is false', () => {
const wrapper = shallowWithIntl(<EmojiPicker {...baseProps}/>);
const instance = wrapper.instance();
instance.setState = jest.fn();
instance.rebuildEmojis = true;
const searchBarAnimationComplete = false;
const setRebuiltEmojis = jest.spyOn(instance, 'setRebuiltEmojis');
setRebuiltEmojis(searchBarAnimationComplete);
expect(instance.setState).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AttachmentImage it matches snapshot 1`] = `
<TouchableWithoutFeedback
onPress={[Function]}
style={
Array [
Object {
"marginTop": 5,
},
Object {
"width": 38,
},
]
}
>
<View
style={
Array [
Object {
"borderColor": "rgba(61,60,64,0.1)",
"borderRadius": 2,
"borderWidth": 1,
"flex": 1,
"padding": 5,
},
Object {
"height": 28,
"width": 28,
},
]
}
>
<Connect(ProgressiveImage)
imageUri="https://images.com/image.png"
resizeMode="contain"
style={
Object {
"height": 28,
"width": 28,
}
}
/>
</View>
</TouchableWithoutFeedback>
`;

View File

@@ -48,6 +48,12 @@ export default class AttachmentImage extends PureComponent {
}
}
componentDidUpdate(prevProps) {
if (this.props.imageUrl && (prevProps.imageUrl !== this.props.imageUrl)) {
ImageCacheManager.cache(null, this.props.imageUrl, this.setImageUrl);
}
}
handlePreviewImage = () => {
const {actions, imageUrl} = this.props;
const {

View File

@@ -0,0 +1,135 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import ImageCacheManager from 'app/utils/image_cache_manager';
import {Image} from 'react-native';
import {shallow} from 'enzyme';
import React from 'react';
const originalCacheFn = ImageCacheManager.cache;
const originalGetSizeFn = Image.getSize;
import Preferences from 'mattermost-redux/constants/preferences';
import AttachmentImage from './attachment_image';
describe('AttachmentImage', () => {
const baseProps = {
actions: {
showModalOverCurrentContext: jest.fn(),
},
deviceHeight: 256,
deviceWidth: 128,
imageMetadata: {width: 32, height: 32},
imageUrl: 'https://images.com/image.png',
theme: Preferences.THEMES.default,
};
afterEach(() => {
Image.getSize = originalGetSizeFn;
ImageCacheManager.cache = originalCacheFn;
});
test('it matches snapshot', () => {
const cacheFn = jest.fn((_, url, callback) => {
callback(url);
});
ImageCacheManager.cache = cacheFn;
const wrapper = shallow(<AttachmentImage {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
});
test('it sets state based on props', () => {
const cacheFn = jest.fn((_, url, callback) => {
callback(url);
});
ImageCacheManager.cache = cacheFn;
const wrapper = shallow(<AttachmentImage {...baseProps}/>);
const state = wrapper.state();
expect(state.hasImage).toBe(true);
expect(state.imageUri).toBe('https://images.com/image.png');
expect(state.originalWidth).toBe(32);
expect(cacheFn).toHaveBeenCalled();
});
test('it does not render image if no imageUrl is provided', () => {
const cacheFn = jest.fn((_, url, callback) => {
callback(url);
});
ImageCacheManager.cache = cacheFn;
const props = {...baseProps, imageUrl: null, imageMetadata: null};
const wrapper = shallow(<AttachmentImage {...props}/>);
const state = wrapper.state();
expect(state.hasImage).toBe(false);
expect(state.imageUri).toBe(null);
expect(cacheFn).not.toHaveBeenCalled();
});
test('it calls Image.getSize if metadata is not present', () => {
const cacheFn = jest.fn((_, url, callback) => {
callback(url);
});
const getSizeFn = jest.fn((_, callback) => {
callback(64, 64);
});
ImageCacheManager.cache = cacheFn;
Image.getSize = getSizeFn;
const props = {...baseProps, imageMetadata: null};
const wrapper = shallow(<AttachmentImage {...props}/>);
const state = wrapper.state();
expect(state.hasImage).toBe(true);
expect(state.imageUri).toBe('https://images.com/image.png');
expect(state.originalWidth).toBe(64);
expect(cacheFn).toHaveBeenCalled();
expect(getSizeFn).toHaveBeenCalled();
});
test('it updates image when imageUrl prop changes', () => {
const cacheFn = jest.fn((_, url, callback) => {
callback(url);
});
ImageCacheManager.cache = cacheFn;
const wrapper = shallow(<AttachmentImage {...baseProps}/>);
wrapper.setProps({
imageUrl: 'https://someothersite.com/picture.png',
imageMetadata: {
width: 96,
height: 96,
},
});
const state = wrapper.state();
expect(state.hasImage).toBe(true);
expect(state.imageUri).toBe('https://someothersite.com/picture.png');
expect(state.originalWidth).toBe(96);
expect(cacheFn).toHaveBeenCalledTimes(2);
});
test('it does not update image when an unrelated prop changes', () => {
const cacheFn = jest.fn((_, url, callback) => {
callback(url);
});
ImageCacheManager.cache = cacheFn;
const wrapper = shallow(<AttachmentImage {...baseProps}/>);
wrapper.setProps({
theme: {...Preferences.THEMES.default},
});
const state = wrapper.state();
expect(state.hasImage).toBe(true);
expect(state.imageUri).toBe('https://images.com/image.png');
expect(state.originalWidth).toBe(32);
expect(cacheFn).toHaveBeenCalledTimes(1);
});
});

View File

@@ -134,6 +134,10 @@ export default class PostList extends PureComponent {
}
}
getItemCount = () => {
return this.props.postIds.length;
};
handleClosePermalink = () => {
const {actions} = this.props;
actions.selectFocusedPostId('');
@@ -295,23 +299,39 @@ export default class PostList extends PureComponent {
scrollToBottom = () => {
setTimeout(() => {
if (this.flatListRef && this.flatListRef.current) {
if (this.flatListRef.current) {
this.flatListRef.current.scrollToOffset({offset: 0, animated: true});
}
}, 250);
};
scrollToInitialIndexIfNeeded = (index) => {
if (!this.hasDoneInitialScroll && this.flatListRef?.current) {
this.hasDoneInitialScroll = true;
this.animationFrameInitialIndex = requestAnimationFrame(() => {
this.flatListRef.current.scrollToIndex({
animated: false,
index,
viewOffset: 0,
viewPosition: 1, // 0 is at bottom
});
});
flatListScrollToIndex = (index) => {
this.flatListRef.current.scrollToIndex({
animated: false,
index,
viewOffset: 0,
viewPosition: 1, // 0 is at bottom
});
}
scrollToIndex = (index) => {
this.animationFrameInitialIndex = requestAnimationFrame(() => {
if (this.flatListRef.current && index > 0 && index <= this.getItemCount()) {
this.flatListScrollToIndex(index);
}
});
};
scrollToInitialIndexIfNeeded = (index, count = 0) => {
if (!this.hasDoneInitialScroll) {
if (index > 0 && index <= this.getItemCount()) {
this.hasDoneInitialScroll = true;
this.scrollToIndex(index);
} else if (count < 3) {
setTimeout(() => {
this.scrollToInitialIndexIfNeeded(index, count + 1);
}, 300);
}
}
};

View File

@@ -55,4 +55,32 @@ describe('PostList', () => {
expect(baseProps.actions.handleSelectChannelByName).toHaveBeenCalled();
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should call flatListScrollToIndex only when ref is set and index is in range', () => {
jest.spyOn(global, 'requestAnimationFrame').mockImplementation((cb) => cb());
const instance = wrapper.instance();
const flatListScrollToIndex = jest.spyOn(instance, 'flatListScrollToIndex');
const indexInRange = baseProps.postIds.length;
const indexOutOfRange = [-1, indexInRange + 1];
instance.flatListRef = {
current: null,
};
instance.scrollToIndex(indexInRange);
expect(flatListScrollToIndex).not.toHaveBeenCalled();
instance.flatListRef = {
current: {
scrollToIndex: jest.fn(),
},
};
for (const index of indexOutOfRange) {
instance.scrollToIndex(index);
expect(flatListScrollToIndex).not.toHaveBeenCalled();
}
instance.scrollToIndex(indexInRange);
expect(flatListScrollToIndex).toHaveBeenCalled();
});
});

View File

@@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {Alert} from 'react-native';
import {shallowWithIntl} from 'test/intl-test-helper';
import Preferences from 'mattermost-redux/constants/preferences';
@@ -13,6 +14,12 @@ import PostTextbox from './post_textbox.ios';
jest.mock('NativeEventEmitter');
jest.mock('Alert', () => {
return {
alert: jest.fn(),
};
});
describe('PostTextBox', () => {
const baseProps = {
actions: {
@@ -88,6 +95,21 @@ describe('PostTextBox', () => {
expect(baseProps.actions.handlePostDraftChanged).toHaveBeenCalledTimes(1);
});
test('should not send multiple alerts when message is too long', () => {
const wrapper = shallowWithIntl(
<PostTextbox {...baseProps}/>
);
const instance = wrapper.instance();
const longString = [...Array(baseProps.maxMessageLength + 2).keys()].map(() => Math.random().toString(36).slice(0, 1)).join('');
instance.handleTextChange(longString);
instance.handleTextChange(longString.slice(1));
expect(Alert.alert).toBeCalled();
expect(Alert.alert).toHaveBeenCalledTimes(1);
});
describe('send button', () => {
test('should initially disable and hide the send button', () => {
const wrapper = shallowWithIntl(

View File

@@ -94,6 +94,7 @@ export default class PostTextBoxBase extends PureComponent {
sendingMessage: false,
top: 0,
value: props.value,
longMessageAlertShown: false,
};
}
@@ -171,19 +172,25 @@ export default class PostTextBoxBase extends PureComponent {
const valueLength = value.trim().length;
if (valueLength > maxMessageLength) {
Alert.alert(
intl.formatMessage({
id: 'mobile.message_length.title',
defaultMessage: 'Message Length',
}),
intl.formatMessage({
id: 'mobile.message_length.message',
defaultMessage: 'Your current message is too long. Current character count: {max}/{count}',
}, {
max: maxMessageLength,
count: valueLength,
})
);
// Check if component is already aware message is too long
if (!this.state.longMessageAlertShown) {
Alert.alert(
intl.formatMessage({
id: 'mobile.message_length.title',
defaultMessage: 'Message Length',
}),
intl.formatMessage({
id: 'mobile.message_length.message',
defaultMessage: 'Your current message is too long. Current character count: {max}/{count}',
}, {
max: maxMessageLength,
count: valueLength,
})
);
this.setState({longMessageAlertShown: true});
}
} else if (this.state.longMessageAlertShown) {
this.setState({longMessageAlertShown: false});
}
};

View File

@@ -18,6 +18,7 @@ import EvilIcon from 'react-native-vector-icons/EvilIcons';
import IonIcon from 'react-native-vector-icons/Ionicons';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {emptyFunction} from 'app/utils/general';
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
const AnimatedIonIcon = Animated.createAnimatedComponent(IonIcon);
@@ -77,6 +78,7 @@ export default class Search extends Component {
shadowVisible: PropTypes.bool,
leftComponent: PropTypes.element,
inputCollapsedMargin: PropTypes.number,
onAnimationComplete: PropTypes.func,
};
static defaultProps = {
@@ -101,6 +103,7 @@ export default class Search extends Component {
value: '',
leftComponent: null,
inputCollapsedMargin: 10,
onAnimationComplete: emptyFunction,
};
constructor(props) {
@@ -244,7 +247,7 @@ export default class Search extends Component {
Animated.timing(
this.inputFocusWidthAnimated,
{
toValue: this.contentWidth - 70,
toValue: this.contentWidth - 90,
duration: 200,
}
),
@@ -265,7 +268,7 @@ export default class Search extends Component {
Animated.timing(
this.btnCancelAnimated,
{
toValue: this.state.leftComponentWidth ? 15 - this.state.leftComponentWidth : 10,
toValue: this.state.leftComponentWidth ? 15 - this.state.leftComponentWidth : 5,
duration: 200,
}
),
@@ -369,7 +372,7 @@ export default class Search extends Component {
useNativeDriver: true,
}
),
]).start();
]).start(({finished}) => this.props.onAnimationComplete(finished));
this.shadowHeight = this.props.shadowOffsetHeightCollapsed;
resolve();
});
@@ -544,17 +547,18 @@ const styles = StyleSheet.create({
position: 'absolute',
paddingLeft: 1,
paddingTop: 3,
right: 65,
right: 80,
width: 25,
},
iconDeleteDefault: {
color: 'grey',
},
cancelButton: {
flex: 1,
justifyContent: 'center',
alignItems: 'flex-start',
alignItems: 'center',
backgroundColor: 'transparent',
width: 60,
minWidth: 75,
height: 50,
},
cancelButtonText: {

View File

@@ -153,6 +153,7 @@ export default class ChannelsList extends PureComponent {
onShowTeams={onShowTeams}
/>
)}
positionRightDelete={5}
/>
</View>
);

View File

@@ -101,7 +101,7 @@ class FilteredList extends Component {
return;
}
searchProfiles(term);
searchProfiles(term, {allow_inactive: true});
searchChannels(currentTeam.id, term);
}, General.SEARCH_TIMEOUT_MILLISECONDS);
}

View File

@@ -16,7 +16,9 @@ import {getTheme, getFavoritesPreferences, getSidebarPreferences} from 'mattermo
import {showCreateOption} from 'mattermost-redux/utils/channel_utils';
import {memoizeResult} from 'mattermost-redux/utils/helpers';
import {isAdmin as checkIsAdmin, isSystemAdmin as checkIsSystemAdmin} from 'mattermost-redux/utils/user_utils';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getConfig, getLicense, hasNewPermissions} from 'mattermost-redux/selectors/entities/general';
import {haveITeamPermission} from 'mattermost-redux/selectors/entities/roles';
import Permissions from 'mattermost-redux/constants/permissions';
import {showModal} from 'app/actions/navigation';
@@ -53,16 +55,20 @@ function mapStateToProps(state) {
sidebarPrefs.favorite_at_top === 'true' && favoriteChannelIds.length,
));
let canJoinPublicChannels = true;
if (hasNewPermissions(state)) {
canJoinPublicChannels = haveITeamPermission(state, {
team: currentTeamId,
permission: Permissions.JOIN_PUBLIC_CHANNELS,
});
}
const canCreatePublicChannels = showCreateOption(state, config, license, currentTeamId, General.OPEN_CHANNEL, isAdmin, isSystemAdmin);
const canCreatePrivateChannels = showCreateOption(state, config, license, currentTeamId, General.PRIVATE_CHANNEL, isAdmin, isSystemAdmin);
return {
canCreatePrivateChannels: showCreateOption(
state,
config,
license,
currentTeamId,
General.PRIVATE_CHANNEL,
isAdmin,
isSystemAdmin
),
canJoinPublicChannels,
canCreatePrivateChannels,
canCreatePublicChannels,
favoriteChannelIds,
theme: getTheme(state),
unreadChannelIds,

View File

@@ -5,6 +5,7 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Dimensions,
findNodeHandle,
InteractionManager,
Keyboard,
Platform,
@@ -23,6 +24,7 @@ import ChannelItem from 'app/components/sidebars/main/channels_list/channel_item
import {DeviceTypes, ListTypes} from 'app/constants';
import {SidebarSectionTypes} from 'app/constants/view';
import BottomSheet from 'app/utils/bottom_sheet';
import {t} from 'app/utils/i18n';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity} from 'app/utils/theme';
@@ -39,7 +41,9 @@ export default class List extends PureComponent {
actions: PropTypes.shape({
showModal: PropTypes.func.isRequired,
}).isRequired,
canJoinPublicChannels: PropTypes.bool.isRequired,
canCreatePrivateChannels: PropTypes.bool.isRequired,
canCreatePublicChannels: PropTypes.bool.isRequired,
favoriteChannelIds: PropTypes.array.isRequired,
onSelectChannel: PropTypes.func.isRequired,
unreadChannelIds: PropTypes.array.isRequired,
@@ -56,6 +60,8 @@ export default class List extends PureComponent {
constructor(props) {
super(props);
this.combinedActionsRef = React.createRef();
this.state = {
sections: this.buildSections(props),
showIndicator: false,
@@ -164,72 +170,51 @@ export default class List extends PureComponent {
};
showCreateChannelOptions = () => {
const {formatMessage} = this.context.intl;
const {
canJoinPublicChannels,
canCreatePrivateChannels,
actions,
canCreatePublicChannels,
} = this.props;
const items = [];
const moreChannels = {
action: this.goToMoreChannels,
text: {
id: 'more_channels.title',
defaultMessage: 'More Channels',
},
};
const createPublicChannel = {
action: this.goToCreatePublicChannel,
text: {
id: 'mobile.create_channel.public',
defaultMessage: 'New Public Channel',
},
};
const createPrivateChannel = {
action: this.goToCreatePrivateChannel,
text: {
id: 'mobile.create_channel.private',
defaultMessage: 'New Private Channel',
},
};
const newConversation = {
action: this.goToDirectMessages,
text: {
id: 'mobile.more_dms.title',
defaultMessage: 'New Conversation',
},
};
const moreChannelsText = formatMessage({id: 'more_channels.title', defaultMessage: 'More Channels'});
const newPublicChannelText = formatMessage({id: 'mobile.create_channel.public', defaultMessage: 'New Public Channel'});
const newPrivateChannelText = formatMessage({id: 'mobile.create_channel.private', defaultMessage: 'New Private Channel'});
const newDirectChannelText = formatMessage({id: 'mobile.more_dms.title', defaultMessage: 'New Conversation'});
const cancelText = formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'});
const options = [];
const actions = [];
items.push(moreChannels, createPublicChannel);
if (canCreatePrivateChannels) {
items.push(createPrivateChannel);
if (canJoinPublicChannels) {
actions.push(this.goToMoreChannels);
options.push(moreChannelsText);
}
items.push(newConversation);
const screen = 'OptionsModal';
const title = '';
const passProps = {
items,
};
const options = {
modalPresentationStyle: 'overCurrentContext',
layout: {
backgroundColor: 'transparent',
},
topBar: {
visible: false,
height: 0,
},
animations: {
showModal: {
enable: false,
},
dismissModal: {
enable: false,
},
},
};
if (canCreatePublicChannels) {
actions.push(this.goToCreatePublicChannel);
options.push(newPublicChannelText);
}
actions.showModal(screen, title, passProps, options);
if (canCreatePrivateChannels) {
actions.push(this.goToCreatePrivateChannel);
options.push(newPrivateChannelText);
}
actions.push(this.goToDirectMessages);
options.push(newDirectChannelText);
options.push(cancelText);
const cancelButtonIndex = options.length - 1;
BottomSheet.showBottomSheetWithOptions({
anchor: this.combinedActionsRef?.current ? findNodeHandle(this.combinedActionsRef.current) : null,
options,
cancelButtonIndex,
}, (value) => {
if (value !== cancelButtonIndex) {
actions[value]();
}
});
};
goToCreatePublicChannel = preventDoubleTap(() => {
@@ -303,7 +288,7 @@ export default class List extends PureComponent {
this.setState({width: width - 40});
};
renderSectionAction = (styles, action) => {
renderSectionAction = (styles, action, anchor) => {
const {theme} = this.props;
return (
<TouchableHighlight
@@ -313,6 +298,7 @@ export default class List extends PureComponent {
>
<MaterialIcon
name='add'
ref={anchor ? this.combinedActionsRef : null}
style={styles.action}
/>
</TouchableHighlight>
@@ -351,6 +337,8 @@ export default class List extends PureComponent {
topSeparator,
} = section;
const anchor = (id === 'sidebar.types.recent' || id === 'mobile.channel_list.channels');
return (
<View>
{topSeparator && this.renderSectionSeparator()}
@@ -358,7 +346,7 @@ export default class List extends PureComponent {
<Text style={styles.title}>
{intl.formatMessage({id, defaultMessage}).toUpperCase()}
</Text>
{action && this.renderSectionAction(styles, action)}
{action && this.renderSectionAction(styles, action, anchor)}
</View>
{bottomSeparator && this.renderSectionSeparator()}
</View>

View File

@@ -105,7 +105,7 @@ export default class ChannelSidebar extends Component {
}
shouldComponentUpdate(nextProps, nextState) {
const {currentTeamId, deviceWidth, isLandscape, teamsCount} = this.props;
const {currentTeamId, deviceWidth, isLandscape, teamsCount, theme} = this.props;
const {openDrawerOffset, isSplitView, permanentSidebar, show, searching} = this.state;
if (nextState.openDrawerOffset !== openDrawerOffset || nextState.show !== show || nextState.searching !== searching) {
@@ -115,6 +115,7 @@ export default class ChannelSidebar extends Component {
return nextProps.currentTeamId !== currentTeamId ||
nextProps.isLandscape !== isLandscape || nextProps.deviceWidth !== deviceWidth ||
nextProps.teamsCount !== teamsCount ||
nextProps.theme !== theme ||
nextState.isSplitView !== isSplitView ||
nextState.permanentSidebar !== permanentSidebar;
}

View File

@@ -57,5 +57,28 @@ describe('MainSidebar', () => {
await wrapper.instance().handlePermanentSidebar();
expect(wrapper.state('permanentSidebar')).toBeDefined();
// Reset to false for subsequent tests
DeviceTypes.IS_TABLET = false;
});
test('should re-render when the theme changes', () => {
const theme = Preferences.THEMES.default;
const newTheme = Preferences.THEMES.organization;
const props = {
...baseProps,
theme,
};
const wrapper = shallow(
<MainSidebar {...props}/>
);
const instance = wrapper.instance();
instance.render = jest.fn();
expect(instance.render).toHaveBeenCalledTimes(0);
wrapper.setProps({theme: newTheme});
expect(instance.render).toHaveBeenCalledTimes(1);
});
});

View File

@@ -101,17 +101,22 @@ function loadTranslation(locale) {
}
if (momentData) {
moment.updateLocale(locale, momentData);
moment.updateLocale(locale.toLowerCase(), momentData);
}
} catch (e) {
console.error('NO Translation found', e); //eslint-disable-line no-console
}
}
export function resetMomentLocale() {
moment.locale(DEFAULT_LOCALE);
}
export function getTranslations(locale) {
if (!TRANSLATIONS[locale]) {
loadTranslation(locale);
}
return TRANSLATIONS[locale] || TRANSLATIONS[DEFAULT_LOCALE];
}

View File

@@ -81,7 +81,7 @@ class EMMProvider {
return false;
}
this.setPerformingAuthentication(false);
this.performingAuthentication = false;
return true;
};
@@ -134,7 +134,7 @@ class EMMProvider {
};
showNotSecuredAlert = (translations) => {
return new Promise((resolve) => {
return new Promise(async (resolve) => {
const options = [];
if (Platform.OS === 'android') {
@@ -152,9 +152,22 @@ class EMMProvider {
style: 'cancel',
});
let message;
if (Platform.OS === 'ios') {
const {supportsFaceId} = await mattermostManaged.supportsFaceId();
if (supportsFaceId) {
message = translations[t('mobile.managed.not_secured.ios')];
} else {
message = translations[t('mobile.managed.not_secured.ios.touchId')];
}
} else {
message = translations[t('mobile.managed.not_secured.android')];
}
Alert.alert(
translations[t('mobile.managed.blocked_by')].replace('{vendor}', this.vendor),
Platform.OS === 'ios' ? translations[t('mobile.managed.not_secured.ios')] : translations[t('mobile.managed.not_secured.android')],
message,
options,
{cancelable: false, onDismiss: resolve},
);

View File

@@ -18,7 +18,7 @@ import {selectDefaultChannel} from 'app/actions/views/channel';
import {showOverlay} from 'app/actions/navigation';
import {loadConfigAndLicense, setDeepLinkURL, startDataCleanup} from 'app/actions/views/root';
import {NavigationTypes, ViewTypes} from 'app/constants';
import {getTranslations} from 'app/i18n';
import {getTranslations, resetMomentLocale} from 'app/i18n';
import mattermostManaged from 'app/mattermost_managed';
import PushNotifications from 'app/push_notifications';
import {getCurrentLocale} from 'app/selectors/i18n';
@@ -40,7 +40,6 @@ class GlobalEventHandler {
EventEmitter.on(General.SERVER_VERSION_CHANGED, this.onServerVersionChanged);
EventEmitter.on(General.CONFIG_CHANGED, this.onServerConfigChanged);
EventEmitter.on(General.SWITCH_TO_DEFAULT_CHANNEL, this.onSwitchToDefaultChannel);
this.turnOnInAppNotificationHandling();
Dimensions.addEventListener('change', this.onOrientationChange);
AppState.addEventListener('change', this.onAppStateChange);
Linking.addEventListener('url', this.onDeepLink);
@@ -77,6 +76,10 @@ class GlobalEventHandler {
this.store = opts.store;
this.launchApp = opts.launchApp;
// onAppStateChange may be called by the AppState listener before we
// configure the global event handler so we manually call it here
this.onAppStateChange('active');
const window = Dimensions.get('window');
this.onOrientationChange({window});
@@ -113,12 +116,12 @@ class GlobalEventHandler {
if (this.store) {
this.store.dispatch(setAppState(isActive));
}
if (isActive && emmProvider.previousAppState === 'background') {
this.appActive();
} else if (isBackground) {
this.appInactive();
if (isActive && (!emmProvider.enabled || emmProvider.previousAppState === 'background')) {
this.appActive();
} else if (isBackground) {
this.appInactive();
}
}
emmProvider.previousAppState = appState;
@@ -142,6 +145,7 @@ class GlobalEventHandler {
this.store.dispatch(setServerVersion(''));
deleteFileCache();
removeAppCredentials();
resetMomentLocale();
PushNotifications.clearNotifications();
@@ -181,7 +185,8 @@ class GlobalEventHandler {
}
if (this.launchApp) {
this.launchApp();
const credentials = await getAppCredentials();
this.launchApp(credentials);
}
};

View File

@@ -6,6 +6,7 @@ import thunk from 'redux-thunk';
import intitialState from 'app/initial_state';
import PushNotification from 'app/push_notifications';
import * as I18n from 'app/i18n';
import GlobalEventHandler from './global_event_handler';
@@ -15,6 +16,12 @@ jest.mock('app/init/credentials', () => ({
removeAppCredentials: jest.fn(),
}));
jest.mock('app/utils/error_handling', () => ({
default: {
initializeErrorHandling: jest.fn(),
},
}));
jest.mock('react-native-notifications', () => ({
addEventListener: jest.fn(),
cancelAllLocalNotifications: jest.fn(),
@@ -29,10 +36,54 @@ GlobalEventHandler.store = store;
// TODO: Add Android test as part of https://mattermost.atlassian.net/browse/MM-17110
describe('GlobalEventHandler', () => {
it('should clear notifications on logout', async () => {
it('should clear notifications and reset moment locale on logout', async () => {
const clearNotifications = jest.spyOn(PushNotification, 'clearNotifications');
const resetMomentLocale = jest.spyOn(I18n, 'resetMomentLocale');
await GlobalEventHandler.onLogout();
expect(clearNotifications).toHaveBeenCalled();
expect(resetMomentLocale).toHaveBeenCalledWith();
});
it('should call onAppStateChange after configuration', () => {
const onAppStateChange = jest.spyOn(GlobalEventHandler, 'onAppStateChange');
GlobalEventHandler.configure({store});
expect(GlobalEventHandler.store).not.toBeNull();
expect(onAppStateChange).toHaveBeenCalledWith('active');
});
it('should handle onAppStateChange to active if the store set', () => {
const appActive = jest.spyOn(GlobalEventHandler, 'appActive');
const appInactive = jest.spyOn(GlobalEventHandler, 'appInactive');
expect(GlobalEventHandler.store).not.toBeNull();
GlobalEventHandler.onAppStateChange('active');
expect(appActive).toHaveBeenCalled();
expect(appInactive).not.toHaveBeenCalled();
});
it('should handle onAppStateChange to background if the store set', () => {
const appActive = jest.spyOn(GlobalEventHandler, 'appActive');
const appInactive = jest.spyOn(GlobalEventHandler, 'appInactive');
expect(GlobalEventHandler.store).not.toBeNull();
GlobalEventHandler.onAppStateChange('background');
expect(appActive).not.toHaveBeenCalled();
expect(appInactive).toHaveBeenCalled();
});
it('should not handle onAppStateChange if the store is not set', () => {
const appActive = jest.spyOn(GlobalEventHandler, 'appActive');
const appInactive = jest.spyOn(GlobalEventHandler, 'appInactive');
GlobalEventHandler.store = null;
GlobalEventHandler.onAppStateChange('active');
expect(appActive).not.toHaveBeenCalled();
expect(appInactive).not.toHaveBeenCalled();
GlobalEventHandler.onAppStateChange('background');
expect(appActive).not.toHaveBeenCalled();
expect(appInactive).not.toHaveBeenCalled();
});
});

View File

@@ -26,8 +26,9 @@ const sharedExtensionStarted = Platform.OS === 'android' && MattermostShare.isOp
export const store = configureStore(initialState);
const init = async () => {
const credentials = await getAppCredentials();
if (EphemeralStore.appStarted) {
launchApp();
launchApp(credentials);
return;
}
@@ -44,17 +45,16 @@ const init = async () => {
}
if (!EphemeralStore.appStarted) {
launchAppAndAuthenticateIfNeeded();
launchAppAndAuthenticateIfNeeded(credentials);
}
};
const launchApp = async () => {
const launchApp = async (credentials) => {
telemetry.start([
'start:select_server_screen',
'start:channel_screen',
]);
const credentials = await getAppCredentials();
if (credentials) {
store.dispatch(loadMe());
store.dispatch(resetToChannel({skipMetrics: true}));
@@ -66,9 +66,9 @@ const launchApp = async () => {
EphemeralStore.appStarted = true;
};
const launchAppAndAuthenticateIfNeeded = async () => {
const launchAppAndAuthenticateIfNeeded = async (credentials) => {
await emmProvider.handleManagedConfig(store);
await launchApp();
await launchApp(credentials);
if (emmProvider.enabled) {
if (emmProvider.jailbreakProtection) {

View File

@@ -66,5 +66,6 @@ export default {
return JailMonkey.trustFall();
},
supportsFaceId: async () => false,
quitApp: MattermostManaged.quitApp,
};

View File

@@ -68,5 +68,6 @@ export default {
return JailMonkey.trustFall();
},
supportsFaceId: MattermostManaged.supportsFaceId,
quitApp: MattermostManaged.quitApp,
};

View File

@@ -127,7 +127,10 @@ class PushNotification {
...notification.getData(),
message: notification.getMessage(),
};
this.handleNotification(info, false, userInteraction);
if (!userInteraction) {
this.handleNotification(info, false, userInteraction);
}
};
onNotificationReceivedForeground = (notification) => {

View File

@@ -72,6 +72,7 @@ export default class ChannelAddMembers extends PureComponent {
enalbed: false,
id: 'add-members',
text: context.intl.formatMessage({id: 'integrations.add', defaultMessage: 'Add'}),
color: props.theme.sidebarHeaderTextColor,
showAsAction: 'always',
};

View File

@@ -64,6 +64,7 @@ export default class ChannelMembers extends PureComponent {
};
this.removeButton = {
color: props.theme.sidebarHeaderTextColor,
enabled: false,
id: 'remove-members',
showAsAction: 'always',

View File

@@ -258,7 +258,6 @@ export default class EditProfile extends PureComponent {
handleRemoveProfileImage = () => {
this.setState({profileImageRemove: true});
this.emitCanUpdateAccount(true);
this.props.actions.dismissModal();
}
uploadProfileImage = async () => {

View File

@@ -69,7 +69,5 @@ describe('edit_profile', () => {
expect(wrapper.state('profileImageRemove')).toEqual(true);
expect(instance.emitCanUpdateAccount).toHaveBeenCalledTimes(1);
expect(instance.emitCanUpdateAccount).toBeCalledWith(true);
expect(baseProps.actions.dismissModal).toHaveBeenCalledTimes(1);
});
});

View File

@@ -234,6 +234,8 @@ exports[`ImagePreview should match snapshot 1`] = `
</AnimatedComponent>
`;
exports[`ImagePreview should match snapshot and not renderDownloadButton for local files 1`] = `null`;
exports[`ImagePreview should match snapshot, renderDownloadButton 1`] = `
<TouchableOpacity
activeOpacity={0.2}

View File

@@ -282,19 +282,16 @@ export default class Downloader extends PureComponent {
if (this.mounted) {
this.setState({
progress: 100,
}, () => {
// need to wait a bit for the progress circle UI to update to the give progress
setTimeout(async () => {
if (this.state.didCancel) {
try {
await RNFetchBlob.fs.unlink(path);
} finally {
this.props.onDownloadCancel();
}
} else {
this.props.onDownloadSuccess();
}, async () => {
if (this.state.didCancel) {
try {
await RNFetchBlob.fs.unlink(path);
} finally {
this.downloadDidCancel();
}
}, 2000);
} else {
this.props.onDownloadSuccess();
}
});
}
@@ -306,7 +303,9 @@ export default class Downloader extends PureComponent {
} catch (error) {
// cancellation throws so we need to catch
if (downloadPath) {
RNFetchBlob.fs.unlink(getLocalFilePathFromFile(downloadPath, file));
RNFetchBlob.fs.unlink(getLocalFilePathFromFile(downloadPath, file)).catch(() => {
// do nothing
});
}
if (error.message !== 'cancelled' && this.mounted) {
this.showDownloadFailedAlert();

View File

@@ -227,6 +227,11 @@ export default class ImagePreview extends PureComponent {
const {canDownloadFiles} = this.props;
const file = this.getCurrentFile();
if (file?.data?.localPath) {
// we already have the file locally we don't need to download it
return null;
}
if (file) {
let icon;
let action = emptyFunction;

View File

@@ -67,6 +67,21 @@ describe('ImagePreview', () => {
expect(wrapper.instance().renderDownloadButton()).toMatchSnapshot();
});
test('should match snapshot and not renderDownloadButton for local files', () => {
const props = {
...baseProps,
files: [{caption: 'Caption 1', source: 'source', data: {localPath: 'path'}}],
};
const wrapper = shallow(
<ImagePreview {...props}/>,
{context: {intl: {formatMessage: jest.fn()}}},
);
expect(wrapper.instance().renderDownloadButton()).toMatchSnapshot();
expect(wrapper.instance().renderDownloadButton()).toBeNull();
});
test('should match state on handleChangeImage', () => {
const wrapper = shallow(
<ImagePreview {...baseProps}/>,

View File

@@ -66,6 +66,7 @@ export default class MoreChannels extends PureComponent {
};
this.rightButton = {
color: props.theme.sidebarHeaderTextColor,
id: 'create-pub-channel',
text: context.intl.formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'}),
showAsAction: 'always',

View File

@@ -325,10 +325,11 @@ export default class MoreDirectMessages extends PureComponent {
};
updateNavigationButtons = (startEnabled, context = this.context) => {
const {actions, componentId} = this.props;
const {actions, componentId, theme} = this.props;
const {formatMessage} = context.intl;
actions.setButtons(componentId, {
rightButtons: [{
color: theme.sidebarHeaderTextColor,
id: START_BUTTON,
text: formatMessage({id: 'mobile.more_dms.start', defaultMessage: 'Start'}),
showAsAction: 'always',

View File

@@ -37,7 +37,7 @@ export default class OptionsModalList extends PureComponent {
if (typeof action === 'function') {
action();
}
}, 100);
}, 250);
});
renderOptions = () => {

View File

@@ -226,11 +226,9 @@ class SSO extends PureComponent {
startInLoadingState={true}
onNavigationStateChange={this.onNavigationStateChange}
onShouldStartLoadWithRequest={() => true}
renderLoading={this.renderLoading}
injectedJavaScript={jsCode}
onLoadEnd={this.onLoadEnd}
onMessage={messagingEnabled ? this.onMessage : null}
useWebKit={this.useWebkit}
useSharedProcessPool={true}
cacheEnabled={true}
/>

View File

@@ -83,6 +83,7 @@ export default class ImageCacheManager {
} catch (e) {
RNFetchBlob.fs.unlink(pathWithPrefix);
notifyAll(uri, uri);
unsubscribe(uri);
return null;
}
} else {

View File

@@ -43,9 +43,13 @@ class PushNotificationUtils {
};
loadFromNotification = async (notification) => {
// Set appStartedFromPushNotification to avoid channel screen to call selectInitialChannel
EphemeralStore.appStartedFromPushNotification = true;
await this.store.dispatch(loadFromPushNotification(notification, true));
if (!EphemeralStore.appStartedFromPushNotification) {
// if we have a componentId means that the app is already initialized
const componentId = EphemeralStore.getNavigationTopComponentId();
if (componentId) {
EventEmitter.emit('close_channel_drawer');
EventEmitter.emit('close_settings_sidebar');
@@ -190,11 +194,11 @@ class PushNotificationUtils {
};
unsubscribeFromStore = this.store.subscribe(waitForHydration);
}
};
getNotification = () => {
return PushNotifications.getNotification();
}
};
}
export default new PushNotificationUtils();

View File

@@ -155,17 +155,34 @@ function capture(captureFunc, store) {
// Don't contact Sentry if we're connected to a server with diagnostics disabled. Note that this will
// still log if we're not connected to any server.
if (config.EnableDiagnostics != null && config.EnableDiagnostics !== 'true') {
if (config && config.EnableDiagnostics != null && config.EnableDiagnostics !== 'true') {
return;
}
Sentry.setUserContext(getUserContext(state));
Sentry.setExtraContext(getExtraContext(state));
Sentry.setTagsContext(getBuildTags(state));
let hasUserContext = false;
const userContext = getUserContext(state);
if (Object.keys(userContext).length) {
hasUserContext = true;
Sentry.setUserContext(userContext);
}
console.warn('Capturing with Sentry at ' + getDsn() + '...'); // eslint-disable-line no-console
const extraContext = getExtraContext(state);
if (Object.keys(extraContext).length) {
Sentry.setExtraContext(extraContext);
}
captureFunc();
const buildTags = getBuildTags(state);
if (Object.keys(buildTags).length) {
Sentry.setTagsContext(buildTags);
}
if (hasUserContext) {
console.warn('Capturing with Sentry at ' + getDsn() + '...'); // eslint-disable-line no-console
captureFunc();
} else {
console.warn('No user context, skipping capture'); // eslint-disable-line no-console
}
} catch (e) {
// Don't want this to get into an infinite loop again...
console.warn('Exception occured while sending to Sentry'); // eslint-disable-line no-console

View File

@@ -135,6 +135,7 @@
"mobile.advanced_settings.delete_title": "Dokumente & Daten löschen",
"mobile.advanced_settings.timezone": "Zeitzone",
"mobile.advanced_settings.title": "Erweiterte Einstellungen",
"mobile.alert_dialog.alertCancel": "Abbrechen",
"mobile.android.camera_permission_denied_description": "Um Fotos und Videos mit ihrer Kamera aufzunehmen, ändern Sie bitte ihre Berechtigungseinstellungen.",
"mobile.android.camera_permission_denied_title": "Kamerazugrif wird benötigt",
"mobile.android.permission_denied_dismiss": "Verwerfen",
@@ -277,6 +278,7 @@
"mobile.managed.jailbreak": "Geräten mit Jailbreak wird von {vendor} nicht vertraut, bitte beenden Sie die App.",
"mobile.managed.not_secured.android": "Dieses Gerät muss mit einer Bildschirmsperre gesichert werden, um Mattermost verwenden zu können.",
"mobile.managed.not_secured.ios": "Dieses Gerät muss mit einem Passcode gesichert werden, um Mattermost verwenden zu können.\n \nGehen Sie zu Einstellungen > Face ID & Passwort.",
"mobile.managed.not_secured.ios.touchId": "Dieses Gerät muss mit einem Passcode gesichert werden, um Mattermost zu verwenden.\n \nGehen Sie zu Einstellungen > Touch ID & Passwort.",
"mobile.managed.secured_by": "Gesichert durch {vendor}",
"mobile.managed.settings": "Zu Einstellungen gehen",
"mobile.markdown.code.copy_code": "Code kopieren",
@@ -351,9 +353,6 @@
"mobile.post_pre_header.flagged": "Markiert",
"mobile.post_pre_header.pinned": "Angeheftet",
"mobile.post_pre_header.pinned_flagged": "Angeheftet und markiert",
"mobile.post_textbox.empty.message": "Sie versuchen, eine leere Nachricht zu senden.\nBitte stellen Sie sicher, dass die Nachricht Text oder eine angehängte Datei enthält.",
"mobile.post_textbox.empty.ok": "OK",
"mobile.post_textbox.empty.title": "Leere Nachricht",
"mobile.post_textbox.uploadFailedDesc": "Einige Anhänge konnten nicht auf den Server hochgeladen werden. Sind Sie sicher, dass sie die Nachricht abschicken wollen?",
"mobile.post_textbox.uploadFailedTitle": "Anhang Fehler",
"mobile.post.cancel": "Abbrechen",

View File

@@ -278,6 +278,7 @@
"mobile.managed.jailbreak": "Jailbroken devices are not trusted by {vendor}, please exit the app.",
"mobile.managed.not_secured.android": "This device must be secured with a screen lock to use Mattermost.",
"mobile.managed.not_secured.ios": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode.",
"mobile.managed.not_secured.ios.touchId": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Touch ID & Passcode.",
"mobile.managed.secured_by": "Secured by {vendor}",
"mobile.managed.settings": "Go to settings",
"mobile.markdown.code.copy_code": "Copy Code",

View File

@@ -135,6 +135,7 @@
"mobile.advanced_settings.delete_title": "Eliminar Documentos & Datos",
"mobile.advanced_settings.timezone": "Zona horaria",
"mobile.advanced_settings.title": "Configuración Avanzada",
"mobile.alert_dialog.alertCancel": "Cancelar",
"mobile.android.camera_permission_denied_description": "Para tomar fotos y videos con la cámara, por favor, cambia la configuración de permisos.",
"mobile.android.camera_permission_denied_title": "Acceso a la cámara es necesario",
"mobile.android.permission_denied_dismiss": "Cerrar",
@@ -277,6 +278,7 @@
"mobile.managed.jailbreak": "{vendor} no confía en los dispositivos con jailbreak, por favor, salga de la aplicación.",
"mobile.managed.not_secured.android": "Este dispositivo debe estar asegurado con un bloqueo de pantalla para utilizar Mattermost.",
"mobile.managed.not_secured.ios": "Este dispositivo debe estar protegido con un código de acceso para utilizar Mattermost.\n\nVaya a Configuración > Identificación Facial y clave de acceso.",
"mobile.managed.not_secured.ios.touchId": "Este dispositivo debe estar protegido con un código de acceso para utilizar Mattermost.\n\nVaya a Configuración > Identificación Táctil y clave de acceso.",
"mobile.managed.secured_by": "Asegurado por {vendor}",
"mobile.managed.settings": "Ir a configuración",
"mobile.markdown.code.copy_code": "Copiar código",
@@ -351,9 +353,6 @@
"mobile.post_pre_header.flagged": "Marcado",
"mobile.post_pre_header.pinned": "Anclado",
"mobile.post_pre_header.pinned_flagged": "Anclado y Marcado",
"mobile.post_textbox.empty.message": "Estas intentando enviar un mensaje vacío.\nPor favor asegurate de estar enviando un mensaje con texto o con al menos un archivo adjunto.",
"mobile.post_textbox.empty.ok": "Aceptar",
"mobile.post_textbox.empty.title": "Mensaje vacío",
"mobile.post_textbox.uploadFailedDesc": "Algunos archivos adjuntos no se han subido al servidor. ¿Quieres publicar el mensaje?",
"mobile.post_textbox.uploadFailedTitle": "Error Adjuntando",
"mobile.post.cancel": "Cancelar",

View File

@@ -135,6 +135,7 @@
"mobile.advanced_settings.delete_title": "Supprimer les documents et les données",
"mobile.advanced_settings.timezone": "Fuseau horaire",
"mobile.advanced_settings.title": "Paramètres avancés",
"mobile.alert_dialog.alertCancel": "Annuler",
"mobile.android.camera_permission_denied_description": "Pour prendre des photos et des vidéos avec votre appareil photo, veuillez modifier vos paramètres d'autorisation.",
"mobile.android.camera_permission_denied_title": "L'accès à la caméra est requis",
"mobile.android.permission_denied_dismiss": "Rejeter",
@@ -277,6 +278,7 @@
"mobile.managed.jailbreak": "Les dispositifs jailbreakés ne sont pas approuvés par {vendor}, veuillez quitter l'application.",
"mobile.managed.not_secured.android": "Cet appareil doit être sécurisé avec un verrouillage d'écran pour pouvoir utiliser Mattermost.",
"mobile.managed.not_secured.ios": "Cet appareil doit être sécurisé avec un code pour pouvoir utiliser Mattermost.\n\nAllez dans Réglages > Face ID et code.",
"mobile.managed.not_secured.ios.touchId": "Cet appareil doit être sécurisé avec un code pour pouvoir utiliser Mattermost.\n\nAllez dans Réglages > Touch ID et code.",
"mobile.managed.secured_by": "Sécurisé par {vendor}",
"mobile.managed.settings": "Aller dans les paramètres",
"mobile.markdown.code.copy_code": "Copier le code",
@@ -351,9 +353,6 @@
"mobile.post_pre_header.flagged": "Marqué d'un indicateur",
"mobile.post_pre_header.pinned": "Épinglé",
"mobile.post_pre_header.pinned_flagged": "Épinglé et marqué d'un indicateur",
"mobile.post_textbox.empty.message": "Vous êtes en train d'envoyer un message vide.\nVeuillez-vous assurer d'avoir spécifié un message ou d'avoir joint au moins un fichier.",
"mobile.post_textbox.empty.ok": "OK",
"mobile.post_textbox.empty.title": "Message vide",
"mobile.post_textbox.uploadFailedDesc": "Certains fichiers joints n'ont pas pu être envoyés au serveur. Voulez-vous vraiment envoyer votre message ?",
"mobile.post_textbox.uploadFailedTitle": "Erreur de pièces jointes",
"mobile.post.cancel": "Annuler",
@@ -535,7 +534,7 @@
"user.settings.general.email": "E-mail",
"user.settings.general.emailCantUpdate": "L'adresse e-mail ne peut être mise à jour qu'en utilisant un navigateur web ou l'application de bureau.",
"user.settings.general.emailGitlabCantUpdate": "La connexion s'effectue par Gitlab. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications par e-mail est {email}.",
"user.settings.general.emailGoogleCantUpdate": "La connexion s'effectue par Gitlab. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications par e-mail est {email} .",
"user.settings.general.emailGoogleCantUpdate": "La connexion s'effectue par Google. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications par e-mail est {email} .",
"user.settings.general.emailHelp2": "L'envoi d'e-mails a été désactivé par votre administrateur système. Aucun e-mail de notification ne peut être envoyé.",
"user.settings.general.emailLdapCantUpdate": "La connexion s'effectue par AD/LDAP. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications par e-mail est {email}.",
"user.settings.general.emailOffice365CantUpdate": "La connexion s'effectue par Office 365. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications par e-mail est {email} .",

View File

@@ -135,6 +135,7 @@
"mobile.advanced_settings.delete_title": "Cancella Documenti e Dati",
"mobile.advanced_settings.timezone": "Fuso orario",
"mobile.advanced_settings.title": "Impostazioni Avanzate",
"mobile.alert_dialog.alertCancel": "Annulla",
"mobile.android.camera_permission_denied_description": "Per scattare foto e girare video con la videocamera, modificare i permessi nelle impostazioni.",
"mobile.android.camera_permission_denied_title": "L'accesso alla videocamera è richiesto",
"mobile.android.permission_denied_dismiss": "Annulla",
@@ -277,6 +278,7 @@
"mobile.managed.jailbreak": "Dispositivi con jailbreak considerati non sicuri da {vendor}, per favore uscire dall'app.",
"mobile.managed.not_secured.android": "Questo dispositivo deve essere protetto con un blocco schermo per utilizzare Mattermost.",
"mobile.managed.not_secured.ios": "Questo dispositivo deve essere protetto da un codice per utilizzare Mattermost.\n\nAndare in Impostazioni > Face ID & Codice.",
"mobile.managed.not_secured.ios.touchId": "Questo dispositivo deve essere protetto da un codice per utilizzare Mattermost.\n\nAndare in Impostazioni > Touch ID & Codice.",
"mobile.managed.secured_by": "Verificato da {vendor}",
"mobile.managed.settings": "Vai alle impostazioni",
"mobile.markdown.code.copy_code": "Copia codice",
@@ -351,9 +353,6 @@
"mobile.post_pre_header.flagged": "Contrassegnato",
"mobile.post_pre_header.pinned": "Bloccato",
"mobile.post_pre_header.pinned_flagged": "Bloccato e Contrassegnato",
"mobile.post_textbox.empty.message": "Stai cercando di inviare un messaggio vuoto.\nAssicurati di aver scritto del testo o di aver allegato almeno un file.",
"mobile.post_textbox.empty.ok": "OK",
"mobile.post_textbox.empty.title": "Messaggio Vuoto",
"mobile.post_textbox.uploadFailedDesc": "Impossibile caricare alcuni allegati, sicuro di voler pubblicare il messaggio?",
"mobile.post_textbox.uploadFailedTitle": "Allegati non caricati",
"mobile.post.cancel": "Annulla",

View File

@@ -135,6 +135,7 @@
"mobile.advanced_settings.delete_title": "ドキュメントとデータの削除",
"mobile.advanced_settings.timezone": "タイムゾーン",
"mobile.advanced_settings.title": "詳細設定",
"mobile.alert_dialog.alertCancel": "キャンセル",
"mobile.android.camera_permission_denied_description": "カメラで写真やビデオを撮影するために権限設定を変更してください。",
"mobile.android.camera_permission_denied_title": "カメラへのアクセスを要求しています",
"mobile.android.permission_denied_dismiss": "破棄",
@@ -277,6 +278,7 @@
"mobile.managed.jailbreak": "Jailbrokenデバイスは{vendor}から信頼されていません。アプリを終了してください。",
"mobile.managed.not_secured.android": "このデバイスでMattermostを使うにはスクリーンロックを設定する必要があります。",
"mobile.managed.not_secured.ios": "このデバイスでMattermostを使うにはパスコードを設定する必要があります。\n\n設定 > Face IDとパスコード から設定してください。",
"mobile.managed.not_secured.ios.touchId": "このデバイスでMattermostを使うにはパスコードを設定する必要があります。\n\n設定 > Touch IDとパスコード から設定してください。",
"mobile.managed.secured_by": "{vendor}により保護されました",
"mobile.managed.settings": "設定へ移動する",
"mobile.markdown.code.copy_code": "コードのコピー",
@@ -351,9 +353,6 @@
"mobile.post_pre_header.flagged": "フラグ済み",
"mobile.post_pre_header.pinned": "ピン留め済み",
"mobile.post_pre_header.pinned_flagged": "ピン留めとフラグ済み",
"mobile.post_textbox.empty.message": "空のメッセージを送信しようとしています。\n何かメッセージか少なくとも一つの添付ファイルがあることを確認してください。",
"mobile.post_textbox.empty.ok": "OK",
"mobile.post_textbox.empty.title": "空のメッセージ",
"mobile.post_textbox.uploadFailedDesc": "サーバーへの添付ファイルのアップロードが失敗しました。メッセージを投稿しますか?",
"mobile.post_textbox.uploadFailedTitle": "添付エラー",
"mobile.post.cancel": "キャンセル",

View File

@@ -135,6 +135,7 @@
"mobile.advanced_settings.delete_title": "데이터 및 캐시삭제",
"mobile.advanced_settings.timezone": "표준 시간대",
"mobile.advanced_settings.title": "고급 설정",
"mobile.alert_dialog.alertCancel": "취소",
"mobile.android.camera_permission_denied_description": "사진이나 비디오를 촬영 하려면, 권한 설정을 변경해 주세요.",
"mobile.android.camera_permission_denied_title": "카메라 접근이 필요합니다.",
"mobile.android.permission_denied_dismiss": "Dismiss",
@@ -277,6 +278,7 @@
"mobile.managed.jailbreak": "Jailbroken devices are not trusted by {vendor}, please exit the app.",
"mobile.managed.not_secured.android": "This device must be secured with a screen lock to use Mattermost.",
"mobile.managed.not_secured.ios": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode.",
"mobile.managed.not_secured.ios.touchId": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Touch ID & Passcode.",
"mobile.managed.secured_by": "Secured by {vendor}",
"mobile.managed.settings": "Go to settings",
"mobile.markdown.code.copy_code": "Copy Code",
@@ -313,7 +315,7 @@
"mobile.notification_settings_mobile.vibrate": "Vibrate",
"mobile.notification_settings.auto_responder_short": "Automatic Replies",
"mobile.notification_settings.auto_responder.default_message": "Hello, I am out of office and unable to respond to messages.",
"mobile.notification_settings.auto_responder.enabled": "Enabled",
"mobile.notification_settings.auto_responder.enabled": "활성화",
"mobile.notification_settings.auto_responder.footer_message": "개인 메시지에 대한 응답을 자동으로 전송되는 사용자 지정 메시지를 설정합니다. 공개, 비공개 채널의 멘션은 자동 응답 기능이 실행되지 않습니다. 자동 응답을 사용하면 상태가 부재중으로 설정되고 이메일 알림, 푸쉬 알림은 해제됩니다.",
"mobile.notification_settings.auto_responder.message_placeholder": "메시지",
"mobile.notification_settings.auto_responder.message_title": "CUSTOM MESSAGE",
@@ -351,9 +353,6 @@
"mobile.post_pre_header.flagged": "Flagged",
"mobile.post_pre_header.pinned": "공지사항",
"mobile.post_pre_header.pinned_flagged": "Pinned and Flagged",
"mobile.post_textbox.empty.message": "You are trying to send an empty message.\nPlease make sure you have a message or at least one attached file.",
"mobile.post_textbox.empty.ok": "확인",
"mobile.post_textbox.empty.title": "Empty Message",
"mobile.post_textbox.uploadFailedDesc": "Some attachments failed to upload to the server. Are you sure you want to post the message?",
"mobile.post_textbox.uploadFailedTitle": "Attachment failure",
"mobile.post.cancel": "취소",

View File

@@ -135,6 +135,7 @@
"mobile.advanced_settings.delete_title": "Delete Documents & Data",
"mobile.advanced_settings.timezone": "Timezone",
"mobile.advanced_settings.title": "Geavanceerde instellingen",
"mobile.alert_dialog.alertCancel": "Annuleren",
"mobile.android.camera_permission_denied_description": "To take photos and videos with your camera, please change your permission settings.",
"mobile.android.camera_permission_denied_title": "Camera access is required",
"mobile.android.permission_denied_dismiss": "Dismiss",
@@ -277,6 +278,7 @@
"mobile.managed.jailbreak": "Jailbroken devices are not trusted by {vendor}, please exit the app.",
"mobile.managed.not_secured.android": "This device must be secured with a screen lock to use Mattermost.",
"mobile.managed.not_secured.ios": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode.",
"mobile.managed.not_secured.ios.touchId": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Touch ID & Passcode.",
"mobile.managed.secured_by": "Secured by {vendor}",
"mobile.managed.settings": "Go to settings",
"mobile.markdown.code.copy_code": "Copy Code",
@@ -351,9 +353,6 @@
"mobile.post_pre_header.flagged": "Flagged",
"mobile.post_pre_header.pinned": "Pinned",
"mobile.post_pre_header.pinned_flagged": "Pinned and Flagged",
"mobile.post_textbox.empty.message": "You are trying to send an empty message.\nPlease make sure you have a message or at least one attached file.",
"mobile.post_textbox.empty.ok": "OK",
"mobile.post_textbox.empty.title": "Empty Message",
"mobile.post_textbox.uploadFailedDesc": "Some attachments failed to upload to the server. Are you sure you want to post the message?",
"mobile.post_textbox.uploadFailedTitle": "Attachment failure",
"mobile.post.cancel": "Annuleren",

View File

@@ -135,6 +135,7 @@
"mobile.advanced_settings.delete_title": "Usuń dokumenty i dane",
"mobile.advanced_settings.timezone": "Strefa czasowa",
"mobile.advanced_settings.title": "Zaawansowane Ustawienia",
"mobile.alert_dialog.alertCancel": "Anuluj",
"mobile.android.camera_permission_denied_description": "Aby robić zdjęcia i filmy używając aparatu, proszę zmień swoje ustawienia uprawnień.",
"mobile.android.camera_permission_denied_title": "Wymagane są uprawnienia Aparatu",
"mobile.android.permission_denied_dismiss": "Odrzuć",
@@ -277,6 +278,7 @@
"mobile.managed.jailbreak": "Urządzenia z Jailbreakiem nie są zaufane przez {vendor}, proszę wyjść z aplikacji.",
"mobile.managed.not_secured.android": "To urządzenie musi zostać zabezpieczone blokadą ekranu, aby użyć Mattermost.",
"mobile.managed.not_secured.ios": "To urządzenie musi zostać zabezpieczone hasłem, aby używać Mattermost.\n\nIdz do Ustawienia > Identyfikacja twarzy i hasło.",
"mobile.managed.not_secured.ios.touchId": "To urządzenie musi zostać zabezpieczone hasłem, aby używać Mattermost.\n\nPrzejdź do Ustawienia > Touch ID i Hasło.",
"mobile.managed.secured_by": "Zabezpieczony przez {vendor}",
"mobile.managed.settings": "Idz do ustawień",
"mobile.markdown.code.copy_code": "Skopiuj Kod",
@@ -351,9 +353,6 @@
"mobile.post_pre_header.flagged": "Oflagowany",
"mobile.post_pre_header.pinned": "Przypięty",
"mobile.post_pre_header.pinned_flagged": "Przypięty i oflagowany",
"mobile.post_textbox.empty.message": "Próbujesz wysłać pustą wiadomość.\nUpewnij się, że masz wiadomość lub co najmniej jeden załączony plik.",
"mobile.post_textbox.empty.ok": "OK",
"mobile.post_textbox.empty.title": "Pusta Wiadomość",
"mobile.post_textbox.uploadFailedDesc": "Niektórych załączników nie udało się przesłać na serwer. Czy na pewno chcesz opublikować wiadomość?",
"mobile.post_textbox.uploadFailedTitle": "Błąd załącznika",
"mobile.post.cancel": "Anuluj",

View File

@@ -135,6 +135,7 @@
"mobile.advanced_settings.delete_title": "Remoer Documentos & Dados",
"mobile.advanced_settings.timezone": "Fuso horário",
"mobile.advanced_settings.title": "Configurações Avançadas",
"mobile.alert_dialog.alertCancel": "Cancelar",
"mobile.android.camera_permission_denied_description": "Para fazer fotos e vides com sua camera, por favor altere as permissões.",
"mobile.android.camera_permission_denied_title": "Acesso a camera é necessário",
"mobile.android.permission_denied_dismiss": "Ignorar",
@@ -277,6 +278,7 @@
"mobile.managed.jailbreak": "Os dispositivos com Jailbroken não são confiáveis para {vendor}, por favor saia do aplicativo.",
"mobile.managed.not_secured.android": "Este dispositivo deve ser protegido com um bloqueio de tela para usar o Mattermost.",
"mobile.managed.not_secured.ios": "Este dispositivo deve ser protegido por uma senha para usar o Mattermost.\n\nVá para Configurações > Face ID & Passcode.",
"mobile.managed.not_secured.ios.touchId": "Este dispositivo deve ser protegido por uma senha para usar o Mattermost.\n\nVá em Ajustes > Touch ID & Código.",
"mobile.managed.secured_by": "Garantido por {vendor}",
"mobile.managed.settings": "Vá para configurações",
"mobile.markdown.code.copy_code": "Copiar Código",
@@ -351,9 +353,6 @@
"mobile.post_pre_header.flagged": "Marcado",
"mobile.post_pre_header.pinned": "Fixado",
"mobile.post_pre_header.pinned_flagged": "Fixado e Marcado",
"mobile.post_textbox.empty.message": "Você está tentando enviar uma mensagem vazia.\nCertifique-se de ter uma mensagem ou pelo menos um arquivo em anexo.",
"mobile.post_textbox.empty.ok": "OK",
"mobile.post_textbox.empty.title": "Mensagem Vazia",
"mobile.post_textbox.uploadFailedDesc": "Alguns anexos não foram enviados para o servidor. Tem certeza de que deseja publicar a mensagem?",
"mobile.post_textbox.uploadFailedTitle": "Falha no anexo",
"mobile.post.cancel": "Cancelar",

View File

@@ -135,6 +135,7 @@
"mobile.advanced_settings.delete_title": "Ștergeți documentele și datele",
"mobile.advanced_settings.timezone": "Fus orar",
"mobile.advanced_settings.title": "Configurări Avansate Şablon",
"mobile.alert_dialog.alertCancel": "Anulare",
"mobile.android.camera_permission_denied_description": "Pentru a face fotografii și videoclipuri cu camera foto, modificați setările de permisiune.",
"mobile.android.camera_permission_denied_title": "Accesul la cameră este necesar",
"mobile.android.permission_denied_dismiss": "Elimină",
@@ -277,6 +278,7 @@
"mobile.managed.jailbreak": "Dispozitivele jailbroken nu au încredere în {vendor}, închideți aplicația.",
"mobile.managed.not_secured.android": "Acest dispozitiv trebuie să fie asigurat cu un dispozitiv de blocare a ecranului pentru a utiliza Mattermost.",
"mobile.managed.not_secured.ios": "Acest dispozitiv trebuie să fie securizat cu un cod de acces pentru a utiliza Mattermost.\n\nAccesați Setări > ID-ul feței și codul de acces.",
"mobile.managed.not_secured.ios.touchId": "Acest dispozitiv trebuie să fie securizat cu un cod de acces pentru a utiliza Mattermost.\n \nAccesați Setări > ID-ul feței și codul de acces.",
"mobile.managed.secured_by": "Securizat de {vendor}",
"mobile.managed.settings": "Mergi la Setări",
"mobile.markdown.code.copy_code": "Copiați codul",
@@ -351,9 +353,6 @@
"mobile.post_pre_header.flagged": "Marcat",
"mobile.post_pre_header.pinned": "Fixat",
"mobile.post_pre_header.pinned_flagged": "Fixat și marcat",
"mobile.post_textbox.empty.message": "Încercați să trimiteți un mesaj gol.\nAsigurați-vă că aveți un mesaj sau cel puțin un fișier atașat.",
"mobile.post_textbox.empty.ok": "OK",
"mobile.post_textbox.empty.title": "Mesaj gol",
"mobile.post_textbox.uploadFailedDesc": "Unele atașări nu au putut fi încărcate pe server. Sigur doriți să încărcați mesajul?",
"mobile.post_textbox.uploadFailedTitle": "Eșec atașament",
"mobile.post.cancel": "Anulare",

View File

@@ -85,7 +85,7 @@
"intro_messages.group_message": "Начало истории групповых сообщений с участниками. Размещённые здесь сообщения и файлы не видны за пределами этой области.",
"intro_messages.noCreator": "Канал: {name}, созданный {date}",
"intro_messages.onlyInvited": " Только приглашенные пользователи могут видеть этот приватный канал.",
"last_users_message.added_to_channel.type": "**добавлены на канал**. Кем: {actor}.",
"last_users_message.added_to_channel.type": "{actor} добавил **added to the channel**.",
"last_users_message.added_to_team.type": "были **добавлены в команду** пользователем {actor}.",
"last_users_message.first": "{firstUser} и ",
"last_users_message.joined_channel.type": "**выполнен вход на канал**",
@@ -135,6 +135,7 @@
"mobile.advanced_settings.delete_title": "Удалить файловый кэш",
"mobile.advanced_settings.timezone": "Часовой пояс",
"mobile.advanced_settings.title": "Дополнительные параметры",
"mobile.alert_dialog.alertCancel": "Отмена",
"mobile.android.camera_permission_denied_description": "Чтобы делать фото и видео, вам нужно разрешить доступ к камере.",
"mobile.android.camera_permission_denied_title": "Требуется доступ к камере",
"mobile.android.permission_denied_dismiss": "Отказать",
@@ -254,7 +255,7 @@
"mobile.file_upload.camera_video": "Снять видео",
"mobile.file_upload.library": "Библиотека изображений",
"mobile.file_upload.max_warning": "Вы можете загрузить не более 5 файлов за раз.",
"mobile.file_upload.unsupportedMimeType": "Only files of the following MIME type can be uploaded:\n{mimeTypes}",
"mobile.file_upload.unsupportedMimeType": "Можно загружать только файлы следующего типа:\n{mimeTypes}",
"mobile.file_upload.video": "Библиотека видео",
"mobile.flagged_posts.empty_description": "Флаги - один из способов, пометки сообщений для последующей деятельности. Ваши флаги не могут быть просмотрены другими пользователями.",
"mobile.flagged_posts.empty_title": "Отмеченные сообщения",
@@ -277,6 +278,7 @@
"mobile.managed.jailbreak": "Устройства с джейлбрейком не являются доверенными {vendor}, выйдите из приложения.",
"mobile.managed.not_secured.android": "This device must be secured with a screen lock to use Mattermost.",
"mobile.managed.not_secured.ios": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode.",
"mobile.managed.not_secured.ios.touchId": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Touch ID & Passcode.",
"mobile.managed.secured_by": "Защищено {vendor}",
"mobile.managed.settings": "Go to settings",
"mobile.markdown.code.copy_code": "Скопировать код",
@@ -351,9 +353,6 @@
"mobile.post_pre_header.flagged": "Отмеченные",
"mobile.post_pre_header.pinned": "Прикреплённые",
"mobile.post_pre_header.pinned_flagged": "Прикреплённые и отмеченные",
"mobile.post_textbox.empty.message": "Вы пытаетесь отправить пустое сообщение.\nПожалуйста, убедитесь, что вы что-то написали или приложили хотя-бы один файл.",
"mobile.post_textbox.empty.ok": "Ок",
"mobile.post_textbox.empty.title": "Пустое сообщение",
"mobile.post_textbox.uploadFailedDesc": "Некоторые вложения не были загружены на сервер. Вы уверены, что хотите отправить сообщение?",
"mobile.post_textbox.uploadFailedTitle": "Ошибка загрузки",
"mobile.post.cancel": "Отмена",

View File

@@ -135,6 +135,7 @@
"mobile.advanced_settings.delete_title": "Belge ve Verileri Sil",
"mobile.advanced_settings.timezone": "Saat Dilimi",
"mobile.advanced_settings.title": "Gelişmiş Ayarlar",
"mobile.alert_dialog.alertCancel": "İptal",
"mobile.android.camera_permission_denied_description": "Kameranız ile fotoğraf ve görüntü çekebilmek için izin ayarlarınızı değiştirmelisiniz.",
"mobile.android.camera_permission_denied_title": "Kameraya erişim izni gereklidir",
"mobile.android.permission_denied_dismiss": "Kapat",
@@ -277,6 +278,7 @@
"mobile.managed.jailbreak": "Jailbreak uygulanmış aygıtlara {vendor} tarafından güvenilmiyor, lütfen uygulamadan çıkın.",
"mobile.managed.not_secured.android": "Bu aygıtta Mattermost kullanılabilmesi için ekran kilidi güvenliğinin etkinleştirilmesi gerekir.",
"mobile.managed.not_secured.ios": "Bu aygıtta Mattermost kullanılabilmesi için parola güvenliğinin etkinleştirilmesi gerekir.\n\nAyarlar > Yüz Tanıma ve Parola bölümünden ayarlayabilirsiniz.",
"mobile.managed.not_secured.ios.touchId": "Bu aygıtta Mattermost kullanılabilmesi için parola güvenliğinin etkinleştirilmesi gerekir.\n\nAyarlar > Touch ID ve Parola bölümünden ayarlayabilirsiniz.",
"mobile.managed.secured_by": "{vendor} tarafından korunuyor",
"mobile.managed.settings": "Ayarlara git",
"mobile.markdown.code.copy_code": "Kodu Kopyala",
@@ -351,9 +353,6 @@
"mobile.post_pre_header.flagged": "İşaretlenmiş",
"mobile.post_pre_header.pinned": "Sabitlenmiş",
"mobile.post_pre_header.pinned_flagged": "Sabitlenmiş ve İşaretlenmiş",
"mobile.post_textbox.empty.message": "Boş bir ileti göndermeye çalışıyorsunuz.\nLütfen bir ileti yazdığınızdan ya da bir dosya eklediğinizden emin olun.",
"mobile.post_textbox.empty.ok": "Tamam",
"mobile.post_textbox.empty.title": "Boş İleti",
"mobile.post_textbox.uploadFailedDesc": "Bazı ek dosyaları sunucuya yüklenemedi. İletiyi göndermek istediğinize emin misiniz?",
"mobile.post_textbox.uploadFailedTitle": "Ek dosya yüklenemedi",
"mobile.post.cancel": "İptal",

View File

@@ -135,6 +135,7 @@
"mobile.advanced_settings.delete_title": "Delete Documents & Data",
"mobile.advanced_settings.timezone": "Часовий пояс",
"mobile.advanced_settings.title": "Додаткові параметри ",
"mobile.alert_dialog.alertCancel": "Відміна",
"mobile.android.camera_permission_denied_description": "Щоб зробити фотографії та відеозаписи з вашою камерою, будь-ласка, змініть налаштування вашого дозволу.",
"mobile.android.camera_permission_denied_title": "Потрібен доступ до камери ",
"mobile.android.permission_denied_dismiss": "Відхилити",
@@ -277,6 +278,7 @@
"mobile.managed.jailbreak": "Пристрої з Jailbroken не є довіреними {vendor}, вийдіть з програми.",
"mobile.managed.not_secured.android": "This device must be secured with a screen lock to use Mattermost.",
"mobile.managed.not_secured.ios": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode.",
"mobile.managed.not_secured.ios.touchId": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Touch ID & Passcode.",
"mobile.managed.secured_by": "Захищено {vendor}",
"mobile.managed.settings": "Go to settings",
"mobile.markdown.code.copy_code": "Копія коду",
@@ -351,9 +353,6 @@
"mobile.post_pre_header.flagged": "Позначено",
"mobile.post_pre_header.pinned": "Прикріплено",
"mobile.post_pre_header.pinned_flagged": "Прикріплений і позначений",
"mobile.post_textbox.empty.message": "Ви намагаєтеся надіслати порожнє повідомлення.\nБудь-ласка, переконайтеся, що у вас є повідомлення або щонайменше один прикріплений файл.",
"mobile.post_textbox.empty.ok": "OK",
"mobile.post_textbox.empty.title": "Порожнє повідомлення",
"mobile.post_textbox.uploadFailedDesc": "Деякі вкладення не змогли завантажити на сервер. Ви впевнені, що хочете опублікувати це повідомлення? ",
"mobile.post_textbox.uploadFailedTitle": "Вкладення несправності",
"mobile.post.cancel": "Відміна ",

View File

@@ -135,6 +135,7 @@
"mobile.advanced_settings.delete_title": "删除文档及数据",
"mobile.advanced_settings.timezone": "时区",
"mobile.advanced_settings.title": "高级设置",
"mobile.alert_dialog.alertCancel": "取消",
"mobile.android.camera_permission_denied_description": "请更改您的权限设定以拍照摄影。",
"mobile.android.camera_permission_denied_title": "需要摄像头权限",
"mobile.android.permission_denied_dismiss": "解除",
@@ -159,8 +160,8 @@
"mobile.channel_info.alertTitleDeleteChannel": "归档 {term}",
"mobile.channel_info.alertTitleLeaveChannel": "离开 {term}",
"mobile.channel_info.alertYes": "是",
"mobile.channel_info.copy_header": "Copy Header",
"mobile.channel_info.copy_purpose": "Copy Purpose",
"mobile.channel_info.copy_header": "复制标题",
"mobile.channel_info.copy_purpose": "复制用途",
"mobile.channel_info.delete_failed": "我们无法归档频道 {displayName}。请检查您的网络连接再尝试。",
"mobile.channel_info.edit": "编辑频道",
"mobile.channel_info.privateChannel": "私有频道",
@@ -277,6 +278,7 @@
"mobile.managed.jailbreak": "越狱的设备不被 {vendor} 信任,请退出应用。",
"mobile.managed.not_secured.android": "此设备必须开启屏幕锁才能使用 Mattermost。",
"mobile.managed.not_secured.ios": "此设备必须开启密码锁才能使用 Mattermost。\n\n前往设置 > 面容 ID 与密码。",
"mobile.managed.not_secured.ios.touchId": "此设备必须开启密码锁才能使用 Mattermost。\n\n前往设置 > 面容 ID 与密码。",
"mobile.managed.secured_by": "被 {vendor} 安全保护",
"mobile.managed.settings": "前往设置",
"mobile.markdown.code.copy_code": "复制代码",
@@ -351,9 +353,6 @@
"mobile.post_pre_header.flagged": "已标记",
"mobile.post_pre_header.pinned": "已置顶",
"mobile.post_pre_header.pinned_flagged": "已被置顶及标记",
"mobile.post_textbox.empty.message": "您正在尝试发送空白消息。\n请确认有消息或至少附加一个附件。",
"mobile.post_textbox.empty.ok": "确定",
"mobile.post_textbox.empty.title": "空消息",
"mobile.post_textbox.uploadFailedDesc": "一些附件上传失败,您确定发送此消息?",
"mobile.post_textbox.uploadFailedTitle": "附件失败",
"mobile.post.cancel": "取消",

View File

@@ -135,6 +135,7 @@
"mobile.advanced_settings.delete_title": "刪除文件與資料",
"mobile.advanced_settings.timezone": "時區",
"mobile.advanced_settings.title": "進階設定",
"mobile.alert_dialog.alertCancel": "取消",
"mobile.android.camera_permission_denied_description": "請變更權限設定以用相機拍照攝影。",
"mobile.android.camera_permission_denied_title": "需要存取相機",
"mobile.android.permission_denied_dismiss": "解除",
@@ -277,6 +278,7 @@
"mobile.managed.jailbreak": "{vendor} 不信任越獄後的裝置,請關閉應用程式。",
"mobile.managed.not_secured.android": "這裝置必須要設定螢幕鎖以使用 Mattermost",
"mobile.managed.not_secured.ios": "這裝置必須要設定密碼以使用 Mattermost前往 設定 > Face ID 與密碼",
"mobile.managed.not_secured.ios.touchId": "這裝置必須要設定密碼以使用 Mattermost。前往 設定 > Touch ID 與密碼",
"mobile.managed.secured_by": "受到 {vendor} 保護",
"mobile.managed.settings": "前往設定",
"mobile.markdown.code.copy_code": "複製代碼",
@@ -351,9 +353,6 @@
"mobile.post_pre_header.flagged": "已被標記",
"mobile.post_pre_header.pinned": "已釘選",
"mobile.post_pre_header.pinned_flagged": "被釘選及標記",
"mobile.post_textbox.empty.message": "正在嘗試發送空白訊息。\n請確定有訊息或是至少有附加 1 個檔案。",
"mobile.post_textbox.empty.ok": "確定",
"mobile.post_textbox.empty.title": "空白訊息",
"mobile.post_textbox.uploadFailedDesc": "部份附加檔案上傳時失敗,請再次確定要發布此訊息?",
"mobile.post_textbox.uploadFailedTitle": "附加檔案失敗",
"mobile.post.cancel": "取消",

View File

@@ -62,7 +62,6 @@
7F240ADB220E089300637665 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F240ADA220E089300637665 /* Item.swift */; };
7F240ADD220E094A00637665 /* TeamsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F240ADC220E094A00637665 /* TeamsViewController.swift */; };
7F2691A11EE1DC6A007574FE /* libRNNotifications.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F2691A01EE1DC51007574FE /* libRNNotifications.a */; };
7F26C1CC219C463D00FEB42D /* libRNCWebView.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F26C1CB219C463300FEB42D /* libRNCWebView.a */; };
7F292A711E8AB73400A450A3 /* SplashScreenResource in Resources */ = {isa = PBXBuildFile; fileRef = 7F292A701E8AB73400A450A3 /* SplashScreenResource */; };
7F292AA61E8ABB1100A450A3 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7F292AA41E8ABB1100A450A3 /* LaunchScreen.xib */; };
7F292AA71E8ABB1100A450A3 /* splash.png in Resources */ = {isa = PBXBuildFile; fileRef = 7F292AA51E8ABB1100A450A3 /* splash.png */; };
@@ -93,6 +92,7 @@
7FABE04622137F5C00D0F595 /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; };
7FABE0562213884700D0F595 /* libUploadAttachments.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FABE04522137F2A00D0F595 /* libUploadAttachments.a */; };
7FBB5E9B1E1F5A4B000DE18A /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF290C1E1F4B4E00DBBE56 /* libRNVectorIcons.a */; };
7FD1DD0823273C6000E0D948 /* libRNCWebView.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F26C1CB219C463300FEB42D /* libRNCWebView.a */; };
7FDB92B11F706F58006CDFD1 /* libRNImagePicker.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDB92A71F706F45006CDFD1 /* libRNImagePicker.a */; };
7FEB10981F6101710039A015 /* BlurAppScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB10971F6101710039A015 /* BlurAppScreen.m */; };
7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FEB109A1F61019C0039A015 /* MattermostManaged.m */; };
@@ -945,7 +945,7 @@
7FDB92B11F706F58006CDFD1 /* libRNImagePicker.a in Frameworks */,
7F5CA9A0208FE3B9004F91CE /* libRNDocumentPicker.a in Frameworks */,
7F43D5DE1F6BF96A001FC614 /* libRCTYouTube.a in Frameworks */,
7F26C1CC219C463D00FEB42D /* libRNCWebView.a in Frameworks */,
7FD1DD0823273C6000E0D948 /* libRNCWebView.a in Frameworks */,
7F88A04422270FD400DC5DE2 /* libRNSentry.a in Frameworks */,
7F43D6061F6BF9EB001FC614 /* libPods-Mattermost.a in Frameworks */,
A9B746D2CFA9CEBFB8AE2B5B /* libPods-Mattermost.a in Frameworks */,
@@ -2785,7 +2785,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 221;
CURRENT_PROJECT_VERSION = 234;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;
@@ -2845,7 +2845,7 @@
CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 221;
CURRENT_PROJECT_VERSION = 234;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = UQ8HT4Q2XM;
ENABLE_BITCODE = NO;

View File

@@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.22.0</string>
<string>1.23.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -34,7 +34,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>221</string>
<string>234</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
@@ -56,6 +56,8 @@
</dict>
<key>NSAppleMusicUsageDescription</key>
<string>Let Mattermost access your Media files</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Share post data <span class="x x-first x-last">across</span> devices with Mattermost</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Share post data accross devices with Mattermost</string>
<key>NSCalendarsUsageDescription</key>

View File

@@ -7,6 +7,7 @@
//
#import "MattermostManaged.h"
#import <LocalAuthentication/LocalAuthentication.h>
#import <UploadAttachments/MMMConstants.h>
@implementation MattermostManaged {
@@ -167,4 +168,17 @@ RCT_EXPORT_METHOD(quitApp)
exit(0);
}
RCT_EXPORT_METHOD(supportsFaceId:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
LAContext *context = [[LAContext alloc] init];
NSError *error;
// context.biometryType is initialized when canEvaluatePolicy, regardless of the error or return value
[context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&error];
resolve(@{
@"supportsFaceId": @(context.biometryType == LABiometryTypeFaceID && @available(iOS 11.0, *))
});
}
@end

View File

@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.22.0</string>
<string>1.23.1</string>
<key>CFBundleVersion</key>
<string>221</string>
<string>234</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>

View File

@@ -47,9 +47,16 @@ class ShareViewController: SLComposeServiceViewController {
var error: NSError?
if !context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
if let error = error, error.code == kLAErrorPasscodeNotSet {
var message = "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Touch ID & Passcode."
if #available(iOS 11.0, *) {
if (context.biometryType == LABiometryType.faceID) {
message = "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode."
}
}
self.showErrorMessage(
title: "",
message: "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode.",
message: message,
VC: self
)
} else {

View File

@@ -15,10 +15,10 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.22.0</string>
<string>1.23.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>221</string>
<string>234</string>
</dict>
</plist>

View File

@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.22.0</string>
<string>1.23.1</string>
<key>CFBundleVersion</key>
<string>221</string>
<string>234</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View File

@@ -174,16 +174,7 @@ RCT_EXPORT_METHOD(
WKHTTPCookieStore *cookieStore = [[WKWebsiteDataStore defaultDataStore] httpCookieStore];
[cookieStore getAllCookies:^(NSArray<NSHTTPCookie *> *allCookies) {
for(NSHTTPCookie *currentCookie in allCookies) {
// Uses the NSHTTPCookie directly has no effect, nor deleted the cookie nor thrown an error.
// Create a new cookie with the given values and delete this one do the work.
NSMutableDictionary<NSHTTPCookiePropertyKey, id> *cookieData = [NSMutableDictionary dictionary];
[cookieData setValue:currentCookie.name forKey:NSHTTPCookieName];
[cookieData setValue:currentCookie.value forKey:NSHTTPCookieValue];
[cookieData setValue:currentCookie.domain forKey:NSHTTPCookieDomain];
[cookieData setValue:currentCookie.path forKey:NSHTTPCookiePath];
NSHTTPCookie *newCookie = [NSHTTPCookie cookieWithProperties:cookieData];
[cookieStore deleteCookie:newCookie completionHandler:^{}];
[cookieStore deleteCookie:currentCookie completionHandler:^{}];
}
resolve(nil);
}];

218
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "mattermost-mobile",
"version": "1.22.0",
"version": "1.23.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -904,9 +904,9 @@
}
},
"@babel/plugin-transform-async-to-generator": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.4.4.tgz",
"integrity": "sha512-YiqW2Li8TXmzgbXw+STsSqPBPFnGviiaSp6CYOq55X8GQ2SGVLrXB6pNid8HkqkZAzOH6knbai3snhP7v0fNwA==",
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz",
"integrity": "sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg==",
"requires": {
"@babel/helper-module-imports": "^7.0.0",
"@babel/helper-plugin-utils": "^7.0.0",
@@ -1119,12 +1119,105 @@
}
},
"@babel/plugin-transform-object-super": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz",
"integrity": "sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg==",
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.5.5.tgz",
"integrity": "sha512-un1zJQAhSosGFBduPgN/YFNvWVpRuHKU7IHBglLoLZsGmruJPOo6pbInneflUdmq7YvSVqhpPs5zdBvLnteltQ==",
"requires": {
"@babel/helper-plugin-utils": "^7.0.0",
"@babel/helper-replace-supers": "^7.1.0"
"@babel/helper-replace-supers": "^7.5.5"
},
"dependencies": {
"@babel/code-frame": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
"integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==",
"requires": {
"@babel/highlight": "^7.0.0"
}
},
"@babel/generator": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.5.tgz",
"integrity": "sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==",
"requires": {
"@babel/types": "^7.5.5",
"jsesc": "^2.5.1",
"lodash": "^4.17.13",
"source-map": "^0.5.0",
"trim-right": "^1.0.1"
}
},
"@babel/helper-member-expression-to-functions": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz",
"integrity": "sha512-5qZ3D1uMclSNqYcXqiHoA0meVdv+xUEex9em2fqMnrk/scphGlGgg66zjMrPJESPwrFJ6sbfFQYUSa0Mz7FabA==",
"requires": {
"@babel/types": "^7.5.5"
}
},
"@babel/helper-replace-supers": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz",
"integrity": "sha512-XvRFWrNnlsow2u7jXDuH4jDDctkxbS7gXssrP4q2nUD606ukXHRvydj346wmNg+zAgpFx4MWf4+usfC93bElJg==",
"requires": {
"@babel/helper-member-expression-to-functions": "^7.5.5",
"@babel/helper-optimise-call-expression": "^7.0.0",
"@babel/traverse": "^7.5.5",
"@babel/types": "^7.5.5"
}
},
"@babel/helper-split-export-declaration": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz",
"integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==",
"requires": {
"@babel/types": "^7.4.4"
}
},
"@babel/parser": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz",
"integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g=="
},
"@babel/traverse": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.5.tgz",
"integrity": "sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==",
"requires": {
"@babel/code-frame": "^7.5.5",
"@babel/generator": "^7.5.5",
"@babel/helper-function-name": "^7.1.0",
"@babel/helper-split-export-declaration": "^7.4.4",
"@babel/parser": "^7.5.5",
"@babel/types": "^7.5.5",
"debug": "^4.1.0",
"globals": "^11.1.0",
"lodash": "^4.17.13"
}
},
"@babel/types": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz",
"integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==",
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
},
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"@babel/plugin-transform-parameters": {
@@ -3550,9 +3643,9 @@
"integrity": "sha512-Aksg16keqrxaluFRZwmo8O8ppP9TFylyCEwBElmxeZ+a6DQAvyMn5nS3n+lgSpkYsrwU2ZGVjDluhkjtBrkEqQ=="
},
"@react-native-community/cli": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-1.10.0.tgz",
"integrity": "sha512-48tIWsMKhwbDsKhe5tYcsspsAy7aR3J/yRjdsVh+M2qkKEASe66Xbhiw5RK2nhfzd1IdOdlIxNMiC+9uad6NMQ==",
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-1.11.2.tgz",
"integrity": "sha512-5NuYd30f5PCTrGUbZLnusZKv5nfTWvTDTRa/3Q4vwdMnUQrhm9sZXWGQ5CnFoQ7cE58EAqhj6/ShXeJF3DZ9uQ==",
"requires": {
"chalk": "^1.1.1",
"commander": "^2.19.0",
@@ -3618,9 +3711,9 @@
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
},
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA=="
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
}
}
},
@@ -3681,9 +3774,9 @@
}
},
"@sentry/cli": {
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.43.0.tgz",
"integrity": "sha512-NnJTT0DjzZB8+CxaisVEEExtns5eHkIFy1nqbM8tzNZ6hREshGf5kFZPPTmHairvaVMk1KWa7w8EqNl7jg3KUA==",
"version": "1.47.1",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-1.47.1.tgz",
"integrity": "sha512-WijaRu1lb99OL6rHee6uOSb1wDyNCbrWcTJoRCuZD83K2fw3U58p68nli/y8CoMwQ55Mrg6CgtY8pmBiuseG0A==",
"requires": {
"fs-copy-file-sync": "^1.1.1",
"https-proxy-agent": "^2.2.1",
@@ -3763,9 +3856,9 @@
}
},
"external-editor": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz",
"integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
"integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
"requires": {
"chardet": "^0.7.0",
"iconv-lite": "^0.4.24",
@@ -3781,9 +3874,9 @@
}
},
"inquirer": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz",
"integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==",
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz",
"integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==",
"requires": {
"ansi-escapes": "^3.2.0",
"chalk": "^2.4.2",
@@ -3791,7 +3884,7 @@
"cli-width": "^2.0.0",
"external-editor": "^3.0.3",
"figures": "^2.0.0",
"lodash": "^4.17.11",
"lodash": "^4.17.12",
"mute-stream": "0.0.7",
"run-async": "^2.2.0",
"rxjs": "^6.4.0",
@@ -3856,9 +3949,9 @@
}
},
"p-limit": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz",
"integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz",
"integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==",
"requires": {
"p-try": "^2.0.0"
}
@@ -4084,9 +4177,9 @@
"dev": true
},
"agent-base": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz",
"integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
"requires": {
"es6-promisify": "^5.0.0"
}
@@ -6216,9 +6309,9 @@
"dev": true
},
"es6-promise": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz",
"integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q=="
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"es6-promisify": {
"version": "5.0.0",
@@ -6512,10 +6605,13 @@
}
},
"eslint-utils": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
"integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==",
"dev": true
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz",
"integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^1.0.0"
}
},
"eslint-visitor-keys": {
"version": "1.0.0",
@@ -7108,9 +7204,9 @@
},
"dependencies": {
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA=="
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
}
}
},
@@ -8600,11 +8696,11 @@
}
},
"https-proxy-agent": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
"integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==",
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.2.tgz",
"integrity": "sha512-c8Ndjc9Bkpfx/vCJueCPy0jlP4ccCCSNDp8xwCZzPjKJUm+B+u9WX2x98Qx4n1PiMNTWo3D7KK5ifNV/yJyRzg==",
"requires": {
"agent-base": "^4.1.0",
"agent-base": "^4.3.0",
"debug": "^3.1.0"
},
"dependencies": {
@@ -8617,9 +8713,9 @@
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
@@ -15255,9 +15351,9 @@
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raven-js": {
"version": "3.27.1",
"resolved": "https://registry.npmjs.org/raven-js/-/raven-js-3.27.1.tgz",
"integrity": "sha512-r/9CwSbaGfBFjo4hGR45DAmrukUKkQ4HdMu80PlVLDY1t8f9b4aaZzTsFegaafu7EGhEYougWDJ9/IcTdYdLXQ=="
"version": "3.27.2",
"resolved": "https://registry.npmjs.org/raven-js/-/raven-js-3.27.2.tgz",
"integrity": "sha512-mFWQcXnhRFEQe5HeFroPaEghlnqy7F5E2J3Fsab189ondqUzcjwSVi7el7F36cr6PvQYXoZ1P2F5CSF2/azeMQ=="
},
"react": {
"version": "16.8.6",
@@ -15292,9 +15388,9 @@
"integrity": "sha512-WUSQJ4P/wWcusaH+zZmbECOk7H5N2pOIl0vzheeornkIMhu+qrNdGFm0bDZLCb0hSF0jf/kH1SgkNGfBdTc4wA=="
},
"react-devtools-core": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-3.6.1.tgz",
"integrity": "sha512-I/LSX+tpeTrGKaF1wXSfJ/kP+6iaP2JfshEjW8LtQBdz6c6HhzOJtjZXhqOUrAdysuey8M1/JgPY1flSVVt8Ig==",
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-3.6.3.tgz",
"integrity": "sha512-+P+eFy/yo8Z/UH9J0DqHZuUM5+RI2wl249TNvMx3J2jpUomLQa4Zxl56GEotGfw3PIP1eI+hVf1s53FlUONStQ==",
"requires": {
"shell-quote": "^1.6.1",
"ws": "^3.3.1"
@@ -15423,9 +15519,9 @@
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
},
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA=="
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
}
}
},
@@ -15624,9 +15720,9 @@
"integrity": "sha512-fzCW5SiYP6qCZyDHebaElHonIFr8NFrZK9JDkxFLnpxMJih4d+HQ4rHyOs0Z4Gb/FjyCVbRH7RtEnjeQ0XffMg=="
},
"react-native-sentry": {
"version": "0.43.1",
"resolved": "https://registry.npmjs.org/react-native-sentry/-/react-native-sentry-0.43.1.tgz",
"integrity": "sha512-oDWPLlXBbTFOEzqqxGihFO3DoBkEiQCQKgkF4av07aMdW+dKJxagwFjQW4vkoclNdK3w0Nfjc8kd7I7loDAcnA==",
"version": "0.43.2",
"resolved": "https://registry.npmjs.org/react-native-sentry/-/react-native-sentry-0.43.2.tgz",
"integrity": "sha512-MDrMkt/ntyQhhn+b3EVUuB/lXvQbSLvCOpkAvioNOzddoyo2R6i3rVZ3aZmPN1oK3O9GFE4tSa0M+eTHni7AaQ==",
"requires": {
"@sentry/wizard": "^0.13.0",
"raven-js": "^3.27.1"
@@ -15868,8 +15964,8 @@
}
},
"react-native-webview": {
"version": "github:mattermost/react-native-webview#34e1dbef70413134ddda85df863c962f24b8826e",
"from": "github:mattermost/react-native-webview#34e1dbef70413134ddda85df863c962f24b8826e",
"version": "github:mattermost/react-native-webview#b5e22940a613869d3999feac9451ee65352f4fbe",
"from": "github:mattermost/react-native-webview#b5e22940a613869d3999feac9451ee65352f4fbe",
"requires": {
"escape-string-regexp": "1.0.5",
"invariant": "2.2.4"

View File

@@ -1,6 +1,6 @@
{
"name": "mattermost-mobile",
"version": "1.22.0",
"version": "1.23.1",
"description": "Mattermost Mobile with React Native",
"repository": "git@github.com:mattermost/mattermost-mobile.git",
"author": "Mattermost, Inc.",
@@ -50,13 +50,13 @@
"react-native-permissions": "1.1.1",
"react-native-safe-area": "0.5.1",
"react-native-section-list-get-item-layout": "2.2.3",
"react-native-sentry": "0.43.1",
"react-native-sentry": "0.43.2",
"react-native-slider": "0.11.0",
"react-native-status-bar-size": "0.3.3",
"react-native-svg": "9.4.0",
"react-native-vector-icons": "6.4.2",
"react-native-video": "4.4.1",
"react-native-webview": "github:mattermost/react-native-webview#34e1dbef70413134ddda85df863c962f24b8826e",
"react-native-webview": "github:mattermost/react-native-webview#b5e22940a613869d3999feac9451ee65352f4fbe",
"react-native-youtube": "github:mattermost/react-native-youtube#3f395b620ae4e05a3f1c6bdeef3a1158e78daadc",
"react-navigation": "3.9.1",
"react-redux": "7.0.3",
@@ -147,4 +147,4 @@
"node_modules/(?!react-native|jail-monkey)"
]
}
}
}

View File

@@ -43,6 +43,9 @@ jest.mock('NativeModules', () => {
addEventListener: jest.fn(),
getCurrentState: jest.fn().mockResolvedValue({isConnected: true}),
},
StatusBarManager: {
getHeight: jest.fn(),
},
};
});
jest.mock('NativeEventEmitter');