forked from Ivasoft/mattermost-mobile
Compare commits
42 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6394f89869 | ||
|
|
31e5e0426e | ||
|
|
81292df787 | ||
|
|
882bc6b32b | ||
|
|
5a6b389b5b | ||
|
|
b60b9985d6 | ||
|
|
8e31c5c1b9 | ||
|
|
1e40d31b30 | ||
|
|
fd1b8ce219 | ||
|
|
62c244cd72 | ||
|
|
af715828b6 | ||
|
|
4b016a5272 | ||
|
|
b7970c3a34 | ||
|
|
6806337b23 | ||
|
|
51e6b1e1aa | ||
|
|
dc7f068b15 | ||
|
|
3daa365e44 | ||
|
|
5f0df6eb49 | ||
|
|
ccc9e7c75c | ||
|
|
61c9110d41 | ||
|
|
bf73bf4ecc | ||
|
|
c04d2e6040 | ||
|
|
1e0ead398f | ||
|
|
352a103b48 | ||
|
|
1f3ffee26f | ||
|
|
8be5649ee6 | ||
|
|
1d75287892 | ||
|
|
96e017e9eb | ||
|
|
44c3910ce6 | ||
|
|
23db3b75e2 | ||
|
|
dddcbefefe | ||
|
|
a7dc68b40b | ||
|
|
0b81a9b4e0 | ||
|
|
e05207412f | ||
|
|
3b909101f2 | ||
|
|
96f5ae009d | ||
|
|
a44032f0fb | ||
|
|
9dd5a1c2ed | ||
|
|
0c42c0d976 | ||
|
|
e8398cb880 | ||
|
|
4d83724092 | ||
|
|
8f8d32ff7a |
@@ -1,6 +1,6 @@
|
||||
# Mattermost Mobile
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (5.9)
|
||||
- **Minimum Server versions:** Current ESR version (5.19)
|
||||
- **Supported iOS versions:** 10.3+
|
||||
- **Supported Android versions:** 7.0+
|
||||
|
||||
|
||||
@@ -75,7 +75,8 @@ import com.android.build.OutputFile
|
||||
project.ext.react = [
|
||||
entryFile: "index.js",
|
||||
bundleConfig: "metro.config.js",
|
||||
enableHermes: true,
|
||||
bundleCommand: "ram-bundle",
|
||||
enableHermes: false,
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
@@ -105,18 +106,8 @@ def enableSeparateBuildPerCPUArchitecture = false
|
||||
*/
|
||||
def enableProguardInReleaseBuilds = false
|
||||
|
||||
/**
|
||||
* The preferred build flavor of JavaScriptCore.
|
||||
*
|
||||
* For example, to use the international variant, you can use:
|
||||
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
|
||||
*
|
||||
* The international variant includes ICU i18n library and necessary data
|
||||
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
|
||||
* give correct results when using with locales other than en-US. Note that
|
||||
* this variant is about 6MiB larger per architecture than default.
|
||||
*/
|
||||
def jscFlavor = 'org.webkit:android-jsc-intl:r241213'
|
||||
// Add v8-android - prebuilt libv8android.so into APK
|
||||
def jscFlavor = 'org.chromium:v8-android:+'
|
||||
|
||||
/**
|
||||
* Whether to enable the Hermes VM.
|
||||
@@ -140,8 +131,8 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
missingDimensionStrategy "RNN.reactNativeVersion", "reactNative60"
|
||||
versionCode 259
|
||||
versionName "1.27.0"
|
||||
versionCode 268
|
||||
versionName "1.28.0"
|
||||
multiDexEnabled = true
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a','arm64-v8a','x86','x86_64'
|
||||
@@ -200,6 +191,11 @@ android {
|
||||
sourceCompatibility 1.8
|
||||
targetCompatibility 1.8
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
// Make sure libjsc.so does not packed in APK
|
||||
exclude "**/libjsc.so"
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -232,6 +228,7 @@ dependencies {
|
||||
def hermesPath = "../../node_modules/hermes-engine/android/";
|
||||
debugImplementation files(hermesPath + "hermes-debug.aar")
|
||||
releaseImplementation files(hermesPath + "hermes-release.aar")
|
||||
unsignedImplementation files(hermesPath + "hermes-release.aar")
|
||||
} else {
|
||||
implementation jscFlavor
|
||||
}
|
||||
@@ -267,6 +264,7 @@ dependencies {
|
||||
implementation project(':@sentry_react-native')
|
||||
implementation project(':react-native-android-open-settings')
|
||||
implementation project(':react-native-haptic-feedback')
|
||||
implementation project(':react-native-permissions')
|
||||
|
||||
implementation project(':react-native-fast-image')
|
||||
// For animated GIF support
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@@ -39,7 +40,7 @@
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="mattermost-beta" />
|
||||
<data android:scheme="mattermost" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
|
||||
@@ -33,6 +33,7 @@ import io.sentry.RNSentryModule;
|
||||
import com.dylanvann.fastimage.FastImageViewPackage;
|
||||
import com.levelasquez.androidopensettings.AndroidOpenSettings;
|
||||
import com.mkuczera.RNReactNativeHapticFeedbackModule;
|
||||
import com.reactnativecommunity.rnpermissions.RNPermissionsModule;
|
||||
|
||||
import com.reactnativecommunity.webview.RNCWebViewPackage;
|
||||
import com.brentvatne.react.ReactVideoPackage;
|
||||
@@ -153,6 +154,8 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
return new AndroidOpenSettings(reactContext);
|
||||
case "RNReactNativeHapticFeedbackModule":
|
||||
return new RNReactNativeHapticFeedbackModule(reactContext);
|
||||
case "RNPermissions":
|
||||
return new RNPermissionsModule(reactContext);
|
||||
default:
|
||||
throw new IllegalArgumentException("Could not find module " + name);
|
||||
}
|
||||
@@ -187,6 +190,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
map.put(NetInfoModule.NAME, new ReactModuleInfo(NetInfoModule.NAME, "com.reactnativecommunity.netinfo.NetInfoModule", false, false, false, false, false));
|
||||
map.put("RNAndroidOpenSettings", new ReactModuleInfo("RNAndroidOpenSettings", "com.levelasquez.androidopensettings.AndroidOpenSettings", false, false, false, false, false));
|
||||
map.put("RNReactNativeHapticFeedbackModule", new ReactModuleInfo("RNReactNativeHapticFeedback", "com.mkuczera.RNReactNativeHapticFeedbackModule", false, false, false, false, false));
|
||||
map.put("RNPermissions", new ReactModuleInfo("RNPermissions", "com.reactnativecommunity.rnpermissions.RNPermissionsModule", false, false, false, false, false));
|
||||
return map;
|
||||
}
|
||||
};
|
||||
@@ -208,7 +212,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
instance = this;
|
||||
|
||||
// Delete any previous temp files created by the app
|
||||
File tempFolder = new File(getApplicationContext().getCacheDir(), "mmShare");
|
||||
File tempFolder = new File(getApplicationContext().getCacheDir(), ShareModule.CACHE_DIR_NAME);
|
||||
RealPathUtil.deleteTempFiles(tempFolder);
|
||||
Log.i("ReactNative", "Cleaning temp cache " + tempFolder.getAbsolutePath());
|
||||
|
||||
|
||||
@@ -16,8 +16,11 @@ import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
import com.mattermost.share.RealPathUtil;
|
||||
import com.mattermost.share.ShareModule;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
public class RNPasteableEditTextOnPasteListener implements RNEditTextOnPasteListener {
|
||||
@@ -83,6 +86,14 @@ public class RNPasteableEditTextOnPasteListener implements RNEditTextOnPasteList
|
||||
// Get fileName
|
||||
String fileName = URLUtil.guessFileName(uri, null, mimeType);
|
||||
|
||||
if (uri.contains(ShareModule.CACHE_DIR_NAME)) {
|
||||
uri = moveToImagesCache(uri, fileName);
|
||||
}
|
||||
|
||||
if (uri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get fileSize
|
||||
long fileSize;
|
||||
try {
|
||||
@@ -119,4 +130,17 @@ public class RNPasteableEditTextOnPasteListener implements RNEditTextOnPasteList
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
private String moveToImagesCache(String src, String fileName) {
|
||||
ReactContext ctx = (ReactContext)mEditText.getContext();
|
||||
String dest = ctx.getCacheDir().getAbsolutePath() + "/Images/" + fileName;
|
||||
|
||||
try {
|
||||
Files.move(Paths.get(src), Paths.get(dest));
|
||||
} catch (Exception err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dest;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ public class RealPathUtil {
|
||||
}
|
||||
|
||||
|
||||
File cacheDir = new File(context.getCacheDir(), "mmShare");
|
||||
File cacheDir = new File(context.getCacheDir(), ShareModule.CACHE_DIR_NAME);
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkdirs();
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
private final OkHttpClient client = new OkHttpClient();
|
||||
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
private final MainApplication mApplication;
|
||||
public static final String CACHE_DIR_NAME = "mmShare";
|
||||
|
||||
public ShareModule(MainApplication application, ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
@@ -67,6 +68,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
@Override
|
||||
public Map<String, Object> getConstants() {
|
||||
HashMap<String, Object> constants = new HashMap<>(1);
|
||||
constants.put("cacheDirName", CACHE_DIR_NAME);
|
||||
constants.put("isOpened", mApplication.sharedExtensionIsOpened);
|
||||
mApplication.sharedExtensionIsOpened = false;
|
||||
return constants;
|
||||
@@ -133,7 +135,7 @@ public class ShareModule extends ReactContextBaseJavaModule {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
if (currentActivity != null) {
|
||||
this.tempFolder = new File(currentActivity.getCacheDir(), "mmShare");
|
||||
this.tempFolder = new File(currentActivity.getCacheDir(), CACHE_DIR_NAME);
|
||||
Intent intent = currentActivity.getIntent();
|
||||
action = intent.getAction();
|
||||
type = intent.getType();
|
||||
|
||||
@@ -44,11 +44,17 @@ allprojects {
|
||||
jcenter()
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
url "$rootDir/../node_modules/react-native/android"
|
||||
// url "$rootDir/../node_modules/react-native/android"
|
||||
|
||||
// Replace AAR from original RN with AAR from react-native-v8
|
||||
url("$rootDir/../node_modules/react-native-v8/dist")
|
||||
}
|
||||
maven {
|
||||
// Local Maven repo containing AARs with JSC library built for Android
|
||||
url "$rootDir/../node_modules/jsc-android/dist"
|
||||
// url "$rootDir/../node_modules/jsc-android/dist"
|
||||
|
||||
// prebuilt libv8android.so
|
||||
url("$rootDir/../node_modules/v8-android/dist")
|
||||
}
|
||||
maven {
|
||||
url "https://jitpack.io"
|
||||
|
||||
@@ -3,6 +3,8 @@ include ':@sentry_react-native'
|
||||
project(':@sentry_react-native').projectDir = new File(rootProject.projectDir, '../node_modules/@sentry/react-native/android')
|
||||
include ':react-native-android-open-settings'
|
||||
project(':react-native-android-open-settings').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-android-open-settings/android')
|
||||
include ':react-native-permissions'
|
||||
project(':react-native-permissions').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-permissions/android')
|
||||
include ':react-native-fast-image'
|
||||
project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fast-image/android')
|
||||
include ':react-native-haptic-feedback'
|
||||
|
||||
@@ -37,6 +37,8 @@ import {
|
||||
} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId, getTeamByName} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {getChannelReachable} from 'app/selectors/channel';
|
||||
|
||||
import telemetry from 'app/telemetry';
|
||||
|
||||
import {
|
||||
@@ -62,12 +64,16 @@ export function loadChannelsIfNecessary(teamId) {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadChannelsByTeamName(teamName) {
|
||||
export function loadChannelsByTeamName(teamName, errorHandler) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {currentTeamId} = state.entities.teams;
|
||||
const team = getTeamByName(state, teamName);
|
||||
|
||||
if (!team && errorHandler) {
|
||||
errorHandler();
|
||||
}
|
||||
|
||||
if (team && team.id !== currentTeamId) {
|
||||
await dispatch(fetchMyChannelsAndMembers(team.id));
|
||||
}
|
||||
@@ -400,7 +406,7 @@ export function handleSelectChannel(channelId, fromPushNotification = false) {
|
||||
};
|
||||
}
|
||||
|
||||
export function handleSelectChannelByName(channelName, teamName) {
|
||||
export function handleSelectChannelByName(channelName, teamName, errorHandler) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {teams: currentTeams, currentTeamId} = state.entities.teams;
|
||||
@@ -409,7 +415,13 @@ export function handleSelectChannelByName(channelName, teamName) {
|
||||
const response = await dispatch(getChannelByNameAndTeamName(teamName || currentTeamName, channelName));
|
||||
const {error, data: channel} = response;
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const reachable = getChannelReachable(state, channelName, teamName);
|
||||
|
||||
if (!reachable && errorHandler) {
|
||||
errorHandler();
|
||||
}
|
||||
|
||||
// Fallback to API response error, if any.
|
||||
if (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
@@ -135,6 +135,9 @@ describe('Actions.Views.Channel', () => {
|
||||
channelSelectors.getCurrentChannelId = jest.fn(() => currentChannelId);
|
||||
channelSelectors.getMyChannelMember = jest.fn(() => ({data: {member: {}}}));
|
||||
|
||||
const appChannelSelectors = require('app/selectors/channel');
|
||||
appChannelSelectors.getChannelReachable = jest.fn(() => true);
|
||||
|
||||
test('handleSelectChannelByName success', async () => {
|
||||
store = mockStore(storeObj);
|
||||
|
||||
@@ -181,6 +184,18 @@ describe('Actions.Views.Channel', () => {
|
||||
expect(receivedChannel).toBe(false);
|
||||
});
|
||||
|
||||
test('handleSelectChannelByName failure from unreachable channel', async () => {
|
||||
appChannelSelectors.getChannelReachable = jest.fn(() => false);
|
||||
|
||||
store = mockStore(storeObj);
|
||||
|
||||
await store.dispatch(handleSelectChannelByName(currentChannelName, currentTeamName));
|
||||
|
||||
const storeActions = store.getActions();
|
||||
const receivedChannel = storeActions.some((action) => action.type === MOCK_RECEIVE_CHANNEL_TYPE);
|
||||
expect(receivedChannel).toBe(false);
|
||||
});
|
||||
|
||||
test('loadPostsIfNecessaryWithRetry for the first time', async () => {
|
||||
store = mockStore(storeObj);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {setAppCredentials} from 'app/init/credentials';
|
||||
import PushNotifications from 'app/push_notifications';
|
||||
import {getDeviceTimezoneAsync} from 'app/utils/timezone';
|
||||
import {setCSRFFromCookie} from 'app/utils/security';
|
||||
import {loadConfigAndLicense} from 'app/actions/views/root';
|
||||
|
||||
export function handleLoginIdChanged(loginId) {
|
||||
return async (dispatch, getState) => {
|
||||
@@ -38,6 +39,8 @@ export function handlePasswordChanged(password) {
|
||||
|
||||
export function handleSuccessfulLogin() {
|
||||
return async (dispatch, getState) => {
|
||||
await dispatch(loadConfigAndLicense());
|
||||
|
||||
const state = getState();
|
||||
const config = getConfig(state);
|
||||
const license = getLicense(state);
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import * as GeneralActions from 'mattermost-redux/actions/general';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
import {
|
||||
handleLoginIdChanged,
|
||||
handlePasswordChanged,
|
||||
handleSuccessfulLogin,
|
||||
} from 'app/actions/views/login';
|
||||
|
||||
jest.mock('app/init/credentials', () => ({
|
||||
@@ -36,7 +39,16 @@ describe('Actions.Views.Login', () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore({});
|
||||
store = mockStore({
|
||||
entities: {
|
||||
users: {
|
||||
currentUserId: 'current-user-id',
|
||||
},
|
||||
general: {
|
||||
config: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('handleLoginIdChanged', () => {
|
||||
@@ -60,4 +72,13 @@ describe('Actions.Views.Login', () => {
|
||||
store.dispatch(handlePasswordChanged(password));
|
||||
expect(store.getActions()).toEqual([action]);
|
||||
});
|
||||
|
||||
test('handleSuccessfulLogin gets config and license ', async () => {
|
||||
const getClientConfig = jest.spyOn(GeneralActions, 'getClientConfig');
|
||||
const getLicenseConfig = jest.spyOn(GeneralActions, 'getLicenseConfig');
|
||||
|
||||
await store.dispatch(handleSuccessfulLogin());
|
||||
expect(getClientConfig).toHaveBeenCalled();
|
||||
expect(getLicenseConfig).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,8 +5,7 @@ exports[`SendButton should change theme backgroundColor to 0.3 opacity 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"justifyContent": "flex-end",
|
||||
"paddingHorizontal": 5,
|
||||
"paddingVertical": 2,
|
||||
"paddingRight": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -17,10 +16,9 @@ exports[`SendButton should change theme backgroundColor to 0.3 opacity 1`] = `
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#166de0",
|
||||
"borderRadius": 4,
|
||||
"height": 28,
|
||||
"height": 32,
|
||||
"justifyContent": "center",
|
||||
"paddingLeft": 3,
|
||||
"width": 72,
|
||||
"width": 80,
|
||||
},
|
||||
Object {
|
||||
"backgroundColor": "rgba(22,109,224,0.3)",
|
||||
@@ -29,9 +27,9 @@ exports[`SendButton should change theme backgroundColor to 0.3 opacity 1`] = `
|
||||
}
|
||||
>
|
||||
<PaperPlane
|
||||
color="#ffffff"
|
||||
height={13}
|
||||
width={15}
|
||||
color="rgba(255,255,255,0.5)"
|
||||
height={16}
|
||||
width={19}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -43,8 +41,7 @@ exports[`SendButton should match snapshot 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"justifyContent": "flex-end",
|
||||
"paddingHorizontal": 5,
|
||||
"paddingVertical": 2,
|
||||
"paddingRight": 8,
|
||||
}
|
||||
}
|
||||
type="opacity"
|
||||
@@ -55,17 +52,16 @@ exports[`SendButton should match snapshot 1`] = `
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#166de0",
|
||||
"borderRadius": 4,
|
||||
"height": 28,
|
||||
"height": 32,
|
||||
"justifyContent": "center",
|
||||
"paddingLeft": 3,
|
||||
"width": 72,
|
||||
"width": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
<PaperPlane
|
||||
color="#ffffff"
|
||||
height={13}
|
||||
width={15}
|
||||
height={16}
|
||||
width={19}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithFeedbackIOS>
|
||||
@@ -77,8 +73,7 @@ exports[`SendButton should render theme backgroundColor 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"justifyContent": "flex-end",
|
||||
"paddingHorizontal": 5,
|
||||
"paddingVertical": 2,
|
||||
"paddingRight": 8,
|
||||
}
|
||||
}
|
||||
type="opacity"
|
||||
@@ -89,17 +84,16 @@ exports[`SendButton should render theme backgroundColor 1`] = `
|
||||
"alignItems": "center",
|
||||
"backgroundColor": "#166de0",
|
||||
"borderRadius": 4,
|
||||
"height": 28,
|
||||
"height": 32,
|
||||
"justifyContent": "center",
|
||||
"paddingLeft": 3,
|
||||
"width": 72,
|
||||
"width": 80,
|
||||
}
|
||||
}
|
||||
>
|
||||
<PaperPlane
|
||||
color="#ffffff"
|
||||
height={13}
|
||||
width={15}
|
||||
height={16}
|
||||
width={19}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithFeedbackIOS>
|
||||
|
||||
@@ -23,7 +23,7 @@ describe('AnnouncementBanner', () => {
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<AnnouncementBanner {...baseProps}/>
|
||||
<AnnouncementBanner {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('AtMention', () => {
|
||||
|
||||
test('should match snapshot, no highlight', () => {
|
||||
const wrapper = shallow(
|
||||
<AtMention {...baseProps}/>
|
||||
<AtMention {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
@@ -25,7 +25,7 @@ describe('AtMention', () => {
|
||||
|
||||
test('should match snapshot, with highlight', () => {
|
||||
const wrapper = shallow(
|
||||
<AtMention {...baseProps}/>
|
||||
<AtMention {...baseProps}/>,
|
||||
);
|
||||
|
||||
wrapper.setState({user: {username: 'John.Smith'}});
|
||||
@@ -34,7 +34,7 @@ describe('AtMention', () => {
|
||||
|
||||
test('should match snapshot, without highlight', () => {
|
||||
const wrapper = shallow(
|
||||
<AtMention {...baseProps}/>
|
||||
<AtMention {...baseProps}/>,
|
||||
);
|
||||
|
||||
wrapper.setState({user: {username: 'Victor.Welch'}});
|
||||
|
||||
@@ -21,7 +21,7 @@ import Permissions from 'react-native-permissions';
|
||||
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {PermissionTypes} from 'app/constants';
|
||||
import emmProvider from 'app/init/emm_provider';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
import {t} from 'app/utils/i18n';
|
||||
import {showModalOverCurrentContext} from 'app/actions/navigation';
|
||||
@@ -178,6 +178,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
|
||||
if (hasCameraPermission) {
|
||||
ImagePicker.launchCamera(options, (response) => {
|
||||
emmProvider.inBackgroundSince = null;
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
@@ -208,10 +209,11 @@ export default class AttachmentButton extends PureComponent {
|
||||
options.mediaType = 'mixed';
|
||||
}
|
||||
|
||||
const hasPhotoPermission = await this.hasPhotoPermission('photo');
|
||||
const hasPhotoPermission = await this.hasPhotoPermission('photo', 'photo');
|
||||
|
||||
if (hasPhotoPermission) {
|
||||
ImagePicker.launchImageLibrary(options, (response) => {
|
||||
emmProvider.inBackgroundSince = null;
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
@@ -244,6 +246,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
};
|
||||
|
||||
ImagePicker.launchImageLibrary(options, (response) => {
|
||||
emmProvider.inBackgroundSince = null;
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
@@ -259,6 +262,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
if (hasPermission) {
|
||||
try {
|
||||
const res = await DocumentPicker.pick({type: [browseFileTypes]});
|
||||
emmProvider.inBackgroundSince = null;
|
||||
if (Platform.OS === 'android') {
|
||||
// For android we need to retrieve the realPath in case the file being imported is from the cloud
|
||||
const newUri = await ShareExtension.getFilePath(res.uri);
|
||||
@@ -283,28 +287,21 @@ export default class AttachmentButton extends PureComponent {
|
||||
if (Platform.OS === 'ios') {
|
||||
const {formatMessage} = this.context.intl;
|
||||
let permissionRequest;
|
||||
const targetSource = source || 'photo';
|
||||
const targetSource = source === 'camera' ? Permissions.PERMISSIONS.IOS.CAMERA : Permissions.PERMISSIONS.IOS.PHOTO_LIBRARY;
|
||||
const hasPermissionToStorage = await Permissions.check(targetSource);
|
||||
|
||||
switch (hasPermissionToStorage) {
|
||||
case PermissionTypes.UNDETERMINED:
|
||||
case Permissions.RESULTS.DENIED:
|
||||
permissionRequest = await Permissions.request(targetSource);
|
||||
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
|
||||
if (permissionRequest !== Permissions.RESULTS.GRANTED) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case PermissionTypes.DENIED: {
|
||||
const canOpenSettings = await Permissions.canOpenSettings();
|
||||
let grantOption = null;
|
||||
if (canOpenSettings) {
|
||||
grantOption = {
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
onPress: () => Permissions.openSettings(),
|
||||
};
|
||||
}
|
||||
case Permissions.RESULTS.BLOCKED: {
|
||||
const grantOption = {
|
||||
text: formatMessage({id: 'mobile.permission_denied_retry', defaultMessage: 'Settings'}),
|
||||
onPress: () => Permissions.openSettings(),
|
||||
};
|
||||
|
||||
const {title, text} = this.getPermissionDeniedMessage(source, mediaType);
|
||||
|
||||
@@ -319,7 +316,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
defaultMessage: 'Don\'t Allow',
|
||||
}),
|
||||
},
|
||||
]
|
||||
],
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -332,18 +329,19 @@ export default class AttachmentButton extends PureComponent {
|
||||
hasStoragePermission = async () => {
|
||||
if (Platform.OS === 'android') {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const storagePermission = Permissions.PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE;
|
||||
let permissionRequest;
|
||||
const hasPermissionToStorage = await Permissions.check('storage');
|
||||
const hasPermissionToStorage = await Permissions.check(storagePermission);
|
||||
|
||||
switch (hasPermissionToStorage) {
|
||||
case PermissionTypes.UNDETERMINED:
|
||||
permissionRequest = await Permissions.request('storage');
|
||||
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
|
||||
case Permissions.RESULTS.DENIED:
|
||||
permissionRequest = await Permissions.request(storagePermission);
|
||||
if (permissionRequest !== Permissions.RESULTS.GRANTED) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case PermissionTypes.DENIED: {
|
||||
const {title, text} = this.getPermissionDeniedMessage('storage');
|
||||
case Permissions.RESULTS.BLOCKED: {
|
||||
const {title, text} = this.getPermissionDeniedMessage(storagePermission);
|
||||
|
||||
Alert.alert(
|
||||
title,
|
||||
@@ -362,7 +360,7 @@ export default class AttachmentButton extends PureComponent {
|
||||
}),
|
||||
onPress: () => AndroidOpenSettings.appDetailsSettings(),
|
||||
},
|
||||
]
|
||||
],
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import Permissions from 'react-native-permissions';
|
||||
import {Alert} from 'react-native';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import {VALID_MIME_TYPES} from 'app/screens/edit_profile/edit_profile';
|
||||
import {PermissionTypes} from 'app/constants';
|
||||
|
||||
import AttachmentButton from './index';
|
||||
|
||||
@@ -28,9 +28,7 @@ describe('AttachmentButton', () => {
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<AttachmentButton {...baseProps}/>
|
||||
);
|
||||
const wrapper = shallow(<AttachmentButton {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
@@ -42,9 +40,7 @@ describe('AttachmentButton', () => {
|
||||
onShowUnsupportedMimeTypeWarning: jest.fn(),
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<AttachmentButton {...props}/>
|
||||
);
|
||||
const wrapper = shallow(<AttachmentButton {...props}/>);
|
||||
|
||||
const file = {
|
||||
type: 'image/gif',
|
||||
@@ -63,9 +59,7 @@ describe('AttachmentButton', () => {
|
||||
onShowUnsupportedMimeTypeWarning: jest.fn(),
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<AttachmentButton {...props}/>
|
||||
);
|
||||
const wrapper = shallow(<AttachmentButton {...props}/>);
|
||||
|
||||
const file = {
|
||||
fileSize: 10,
|
||||
@@ -79,11 +73,24 @@ describe('AttachmentButton', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should show permission denied alert if permission is denied in iOS', async () => {
|
||||
expect.assertions(1);
|
||||
test('should return permission false if permission is denied in iOS', async () => {
|
||||
jest.spyOn(Permissions, 'check').mockReturnValue(Permissions.RESULTS.DENIED);
|
||||
jest.spyOn(Permissions, 'request').mockReturnValue(Permissions.RESULTS.DENIED);
|
||||
|
||||
jest.spyOn(Permissions, 'check').mockReturnValue(PermissionTypes.DENIED);
|
||||
jest.spyOn(Permissions, 'canOpenSettings').mockReturnValue(true);
|
||||
const wrapper = shallow(
|
||||
<AttachmentButton {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
const hasPhotoPermission = await wrapper.instance().hasPhotoPermission('camera');
|
||||
expect(Permissions.check).toHaveBeenCalled();
|
||||
expect(Permissions.request).toHaveBeenCalled();
|
||||
expect(Alert.alert).not.toHaveBeenCalled();
|
||||
expect(hasPhotoPermission).toBe(false);
|
||||
});
|
||||
|
||||
test('should show permission denied alert and return permission false if permission is blocked in iOS', async () => {
|
||||
jest.spyOn(Permissions, 'check').mockReturnValue(Permissions.RESULTS.BLOCKED);
|
||||
jest.spyOn(Alert, 'alert').mockReturnValue(true);
|
||||
|
||||
const wrapper = shallow(
|
||||
@@ -91,7 +98,10 @@ describe('AttachmentButton', () => {
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
await wrapper.instance().hasPhotoPermission('camera');
|
||||
expect(Alert.alert).toBeCalled();
|
||||
const hasPhotoPermission = await wrapper.instance().hasPhotoPermission('camera');
|
||||
expect(Permissions.check).toHaveBeenCalled();
|
||||
expect(Permissions.request).not.toHaveBeenCalled();
|
||||
expect(Alert.alert).toHaveBeenCalled();
|
||||
expect(hasPhotoPermission).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -94,13 +94,18 @@ export default class EmojiSuggestion extends PureComponent {
|
||||
|
||||
const results = await fuse.search(matchTerm.toLowerCase());
|
||||
const data = results.map((index) => emojis[index]);
|
||||
this.setEmojiData(data);
|
||||
this.setEmojiData(data, matchTerm);
|
||||
};
|
||||
|
||||
setEmojiData = (data) => {
|
||||
setEmojiData = (data, matchTerm = null) => {
|
||||
let sorter = compareEmojis;
|
||||
if (matchTerm) {
|
||||
sorter = (a, b) => compareEmojis(a, b, matchTerm);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
active: data.length > 0,
|
||||
dataSource: data.sort(compareEmojis),
|
||||
dataSource: data.sort(sorter),
|
||||
});
|
||||
|
||||
this.props.onResultCountChange(data.length);
|
||||
|
||||
@@ -24,7 +24,7 @@ const getEmojisByName = createSelector(
|
||||
}
|
||||
|
||||
return Array.from(emoticons);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function mapStateToProps(state) {
|
||||
|
||||
@@ -23,13 +23,12 @@ const mobileCommandsSelector = createSelector(
|
||||
getAutocompleteCommandsList,
|
||||
(commands) => {
|
||||
return commands.filter((command) => !COMMANDS_TO_HIDE_ON_MOBILE.includes(command.trigger));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
commands: mobileCommandsSelector(state),
|
||||
commandsRequest: state.requests.integrations.getAutocompleteCommands,
|
||||
currentTeamId: getCurrentTeamId(state),
|
||||
theme: getTheme(state),
|
||||
isLandscape: isLandscape(state),
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {Component} from 'react';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
FlatList,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
@@ -18,14 +16,13 @@ import SlashSuggestionItem from './slash_suggestion_item';
|
||||
const SLASH_REGEX = /(^\/)([a-zA-Z-]*)$/;
|
||||
const TIME_BEFORE_NEXT_COMMAND_REQUEST = 1000 * 60 * 5;
|
||||
|
||||
export default class SlashSuggestion extends Component {
|
||||
export default class SlashSuggestion extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
getAutocompleteCommands: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
currentTeamId: PropTypes.string.isRequired,
|
||||
commands: PropTypes.array,
|
||||
commandsRequest: PropTypes.object.isRequired,
|
||||
isSearch: PropTypes.bool,
|
||||
maxListHeight: PropTypes.number,
|
||||
theme: PropTypes.object.isRequired,
|
||||
@@ -56,7 +53,6 @@ export default class SlashSuggestion extends Component {
|
||||
const {currentTeamId} = this.props;
|
||||
const {
|
||||
commands: nextCommands,
|
||||
commandsRequest: nextCommandsRequest,
|
||||
currentTeamId: nextTeamId,
|
||||
value: nextValue,
|
||||
} = nextProps;
|
||||
@@ -81,7 +77,7 @@ export default class SlashSuggestion extends Component {
|
||||
|
||||
const dataIsStale = Date.now() - this.state.lastCommandRequest > TIME_BEFORE_NEXT_COMMAND_REQUEST;
|
||||
|
||||
if ((!nextCommands.length || dataIsStale) && nextCommandsRequest.status !== RequestStatus.STARTED) {
|
||||
if ((!nextCommands.length || dataIsStale)) {
|
||||
this.props.actions.getAutocompleteCommands(nextProps.currentTeamId);
|
||||
this.setState({
|
||||
lastCommandRequest: Date.now(),
|
||||
|
||||
@@ -18,7 +18,7 @@ describe('Badge', () => {
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<Badge {...baseProps}/>
|
||||
<Badge {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.instance().renderText()).toMatchSnapshot();
|
||||
|
||||
@@ -24,7 +24,7 @@ function makeMapStateToProps() {
|
||||
(currentUserId, profilesInChannel) => {
|
||||
const currentChannelMembers = profilesInChannel || [];
|
||||
return currentChannelMembers.filter((m) => m.id !== currentUserId);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
|
||||
@@ -24,7 +24,7 @@ function makeGetChannelNamesMap() {
|
||||
}
|
||||
|
||||
return channelsNameMap;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ export default class ChannelLoader extends PureComponent {
|
||||
|
||||
stopLoadingAnimation = () => {
|
||||
Animated.timing(
|
||||
this.state.barsOpacity
|
||||
this.state.barsOpacity,
|
||||
).stop();
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ export default class ClientUpgradeListener extends PureComponent {
|
||||
intl.formatMessage({
|
||||
id: 'mobile.client_upgrade.download_error.message',
|
||||
defaultMessage: 'An error occurred while trying to open the download link.',
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
return false;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-confusing-arrow */
|
||||
export const ConditionalWrapper = ({conditional, wrapper, children}) => conditional ? wrapper(children) : children;
|
||||
@@ -29,7 +29,7 @@ describe('CustomList', () => {
|
||||
|
||||
test('should match snapshot with FlatList', () => {
|
||||
const wrapper = shallow(
|
||||
<CustomList {...baseProps}/>
|
||||
<CustomList {...baseProps}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find('FlatList')).toHaveLength(1);
|
||||
@@ -42,7 +42,7 @@ describe('CustomList', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<CustomList {...props}/>
|
||||
<CustomList {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find('SectionList')).toHaveLength(1);
|
||||
@@ -50,7 +50,7 @@ describe('CustomList', () => {
|
||||
|
||||
test('should match snapshot, renderSectionHeader', () => {
|
||||
const wrapper = shallow(
|
||||
<CustomList {...baseProps}/>
|
||||
<CustomList {...baseProps}/>,
|
||||
);
|
||||
const section = {
|
||||
id: 'section_id',
|
||||
@@ -61,7 +61,7 @@ describe('CustomList', () => {
|
||||
test('should call props.renderItem on renderItem', () => {
|
||||
const props = {...baseProps};
|
||||
const wrapper = shallow(
|
||||
<CustomList {...props}/>
|
||||
<CustomList {...props}/>,
|
||||
);
|
||||
wrapper.instance().renderItem({item: {id: 'item_id', selected: true}, index: 0, section: null});
|
||||
expect(props.renderItem).toHaveBeenCalledTimes(1);
|
||||
@@ -69,7 +69,7 @@ describe('CustomList', () => {
|
||||
|
||||
test('should match snapshot, renderSeparator', () => {
|
||||
const wrapper = shallow(
|
||||
<CustomList {...baseProps}/>
|
||||
<CustomList {...baseProps}/>,
|
||||
);
|
||||
expect(wrapper.instance().renderSeparator()).toMatchSnapshot();
|
||||
});
|
||||
@@ -77,7 +77,7 @@ describe('CustomList', () => {
|
||||
test('should match snapshot, renderFooter', () => {
|
||||
const props = {...baseProps};
|
||||
const wrapper = shallow(
|
||||
<CustomList {...props}/>
|
||||
<CustomList {...props}/>,
|
||||
);
|
||||
|
||||
// should return null
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('EditChannelInfo', () => {
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<EditChannelInfo {...baseProps}/>
|
||||
<EditChannelInfo {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
@@ -45,7 +45,7 @@ describe('EditChannelInfo', () => {
|
||||
|
||||
test('should have called onHeaderChangeText on text change from Autocomplete', () => {
|
||||
const wrapper = shallow(
|
||||
<EditChannelInfo {...baseProps}/>
|
||||
<EditChannelInfo {...baseProps}/>,
|
||||
);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
@@ -67,7 +67,7 @@ describe('EditChannelInfo', () => {
|
||||
|
||||
test('should call scrollHeaderToTop', () => {
|
||||
const wrapper = shallow(
|
||||
<EditChannelInfo {...baseProps}/>
|
||||
<EditChannelInfo {...baseProps}/>,
|
||||
);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
makeStyleSheetFromTheme,
|
||||
changeOpacity,
|
||||
} from 'app/utils/theme';
|
||||
import {compareEmojis} from 'app/utils/emoji_utils';
|
||||
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
import EmojiPickerRow from './emoji_picker_row';
|
||||
|
||||
@@ -211,7 +212,9 @@ export default class EmojiPicker extends PureComponent {
|
||||
}
|
||||
|
||||
const results = fuse.search(searchTermLowerCase);
|
||||
const data = results.map((index) => emojis[index]);
|
||||
const sorter = (a, b) => compareEmojis(a, b, searchTerm);
|
||||
const data = results.map((index) => emojis[index]).sort(sorter);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ const getEmojisBySection = createSelector(
|
||||
}
|
||||
|
||||
return emoticons;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const getEmojisByName = createSelector(
|
||||
@@ -137,7 +137,7 @@ const getEmojisByName = createSelector(
|
||||
}
|
||||
|
||||
return Array.from(emoticons);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function mapStateToProps(state) {
|
||||
|
||||
@@ -31,7 +31,7 @@ export default class Fade extends PureComponent {
|
||||
toValue: prevProps.visible ? 0 : 1,
|
||||
duration: this.props.duration || FADE_DURATION,
|
||||
useNativeDriver: true,
|
||||
}
|
||||
},
|
||||
).start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('Fade', () => {
|
||||
{...props}
|
||||
>
|
||||
<Text>{dummyText}</Text>
|
||||
</Fade>
|
||||
</Fade>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('FileAttachment', () => {
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<FileAttachment {...baseProps}/>
|
||||
<FileAttachment {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
|
||||
@@ -256,7 +256,7 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK',
|
||||
}),
|
||||
}]
|
||||
}],
|
||||
);
|
||||
this.onDonePreviewingFile();
|
||||
RNFetchBlob.fs.unlink(path);
|
||||
@@ -303,7 +303,7 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK',
|
||||
}),
|
||||
}]
|
||||
}],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -324,7 +324,7 @@ export default class FileAttachmentDocument extends PureComponent {
|
||||
id: 'mobile.server_upgrade.button',
|
||||
defaultMessage: 'OK',
|
||||
}),
|
||||
}]
|
||||
}],
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Platform, StyleSheet, Text, View} from 'react-native';
|
||||
import {Text, View} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import {AnimatedCircularProgress} from 'react-native-circular-progress';
|
||||
|
||||
@@ -17,6 +17,7 @@ import mattermostBucket from 'app/mattermost_bucket';
|
||||
import {buildFileUploadData, encodeHeaderURIStringToUTF8} from 'app/utils/file';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class FileUploadItem extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -163,15 +164,14 @@ export default class FileUploadItem extends PureComponent {
|
||||
};
|
||||
|
||||
renderProgress = (fill) => {
|
||||
const styles = getStyleSheet(this.props.theme);
|
||||
const realFill = Number(fill.toFixed(0));
|
||||
|
||||
return (
|
||||
<View style={styles.progressContent}>
|
||||
<View style={styles.progressCirclePercentage}>
|
||||
<Text style={styles.progressText}>
|
||||
{`${realFill}%`}
|
||||
</Text>
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.progressText}>
|
||||
{`${realFill}%`}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -184,23 +184,28 @@ export default class FileUploadItem extends PureComponent {
|
||||
theme,
|
||||
} = this.props;
|
||||
const {progress} = this.state;
|
||||
const styles = getStyleSheet(theme);
|
||||
let filePreviewComponent;
|
||||
|
||||
if (this.isImageType()) {
|
||||
filePreviewComponent = (
|
||||
<FileAttachmentImage
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
<View style={styles.filePreview}>
|
||||
<FileAttachmentImage
|
||||
file={file}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
filePreviewComponent = (
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={theme}
|
||||
wrapperHeight={100}
|
||||
wrapperWidth={100}
|
||||
/>
|
||||
<View style={styles.filePreview}>
|
||||
<FileAttachmentIcon
|
||||
file={file}
|
||||
theme={theme}
|
||||
wrapperHeight={53}
|
||||
wrapperWidth={53}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,7 +214,7 @@ export default class FileUploadItem extends PureComponent {
|
||||
key={file.clientId}
|
||||
style={styles.preview}
|
||||
>
|
||||
<View style={styles.previewShadow}>
|
||||
<View style={styles.previewContainer}>
|
||||
{filePreviewComponent}
|
||||
{file.failed &&
|
||||
<FileUploadRetry
|
||||
@@ -220,13 +225,12 @@ export default class FileUploadItem extends PureComponent {
|
||||
{file.loading && !file.failed &&
|
||||
<View style={styles.progressCircleContent}>
|
||||
<AnimatedCircularProgress
|
||||
size={100}
|
||||
size={36}
|
||||
fill={progress}
|
||||
width={4}
|
||||
width={2}
|
||||
backgroundColor='rgba(255, 255, 255, 0.5)'
|
||||
tintColor='white'
|
||||
rotation={0}
|
||||
style={styles.progressCircle}
|
||||
>
|
||||
{this.renderProgress}
|
||||
</AnimatedCircularProgress>
|
||||
@@ -245,59 +249,35 @@ export default class FileUploadItem extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
||||
preview: {
|
||||
justifyContent: 'flex-end',
|
||||
height: 115,
|
||||
width: 115,
|
||||
paddingTop: 5,
|
||||
marginLeft: 12,
|
||||
},
|
||||
previewShadow: {
|
||||
height: 100,
|
||||
width: 100,
|
||||
elevation: 10,
|
||||
borderRadius: 5,
|
||||
...Platform.select({
|
||||
ios: {
|
||||
backgroundColor: '#fff',
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 4,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
progressCircle: {
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
previewContainer: {
|
||||
height: 56,
|
||||
width: 56,
|
||||
borderRadius: 4,
|
||||
},
|
||||
progressCircleContent: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.4)',
|
||||
height: 100,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
height: 56,
|
||||
width: 56,
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
width: 100,
|
||||
},
|
||||
progressCirclePercentage: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
progressContent: {
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
borderRadius: 4,
|
||||
},
|
||||
progressText: {
|
||||
color: 'white',
|
||||
fontSize: 18,
|
||||
fontSize: 11,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
filePreview: {
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.15),
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
width: 56,
|
||||
height: 56,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import ImageCacheManager from 'app/utils/image_cache_manager';
|
||||
import FileUploadItem from './file_upload_item';
|
||||
|
||||
@@ -18,7 +19,7 @@ describe('FileUploadItem', () => {
|
||||
file: {
|
||||
loading: false,
|
||||
},
|
||||
theme: {},
|
||||
theme: Preferences.THEMES.default,
|
||||
};
|
||||
|
||||
describe('downloadAndUploadFile', () => {
|
||||
|
||||
@@ -4,19 +4,25 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
InteractionManager,
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
import FileUploadItem from './file_upload_item';
|
||||
|
||||
const showFiles = {opacity: 1, height: 68};
|
||||
const hideFiles = {opacity: 0, height: 0};
|
||||
const hideError = {height: 0};
|
||||
|
||||
export default class FileUploadPreview extends PureComponent {
|
||||
static propTypes = {
|
||||
channelId: PropTypes.string.isRequired,
|
||||
@@ -36,9 +42,17 @@ export default class FileUploadPreview extends PureComponent {
|
||||
showFileMaxWarning: false,
|
||||
};
|
||||
|
||||
errorRef = React.createRef();
|
||||
errorContainerRef = React.createRef();
|
||||
containerRef = React.createRef();
|
||||
|
||||
componentDidMount() {
|
||||
EventEmitter.on('fileMaxWarning', this.handleFileMaxWarning);
|
||||
EventEmitter.on('fileSizeWarning', this.handleFileSizeWarning);
|
||||
|
||||
if (this.props.files.length) {
|
||||
InteractionManager.runAfterInteractions(this.showOrHideContainer);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -46,6 +60,12 @@ export default class FileUploadPreview extends PureComponent {
|
||||
EventEmitter.off('fileSizeWarning', this.handleFileSizeWarning);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.containerRef.current && this.props.files.length !== prevProps.files.length) {
|
||||
InteractionManager.runAfterInteractions(this.showOrHideContainer);
|
||||
}
|
||||
}
|
||||
|
||||
buildFilePreviews = () => {
|
||||
return this.props.files.map((file) => {
|
||||
return (
|
||||
@@ -60,35 +80,77 @@ export default class FileUploadPreview extends PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
clearErrorsFromState = (delay) => {
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
showFileMaxWarning: false,
|
||||
fileSizeWarning: null,
|
||||
});
|
||||
}, delay || 0);
|
||||
}
|
||||
|
||||
handleFileMaxWarning = () => {
|
||||
this.setState({showFileMaxWarning: true});
|
||||
setTimeout(() => {
|
||||
this.setState({showFileMaxWarning: false});
|
||||
}, 3000);
|
||||
if (this.errorRef.current) {
|
||||
this.makeErrorVisible(true, 20);
|
||||
setTimeout(() => {
|
||||
this.makeErrorVisible(false, 20);
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
handleFileSizeWarning = (message) => {
|
||||
this.setState({fileSizeWarning: message});
|
||||
if (this.errorRef.current) {
|
||||
if (message) {
|
||||
this.setState({fileSizeWarning: message.replace(': ', ':\n')});
|
||||
this.makeErrorVisible(true, 40);
|
||||
} else {
|
||||
this.makeErrorVisible(false, 20);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
makeErrorVisible = (visible, height) => {
|
||||
if (this.errorContainerRef.current) {
|
||||
if (visible) {
|
||||
this.errorContainerRef.current.transition(hideError, {height}, 200, 'ease-out');
|
||||
} else {
|
||||
this.errorContainerRef.current.transition({height}, hideError, 200, 'ease-in');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showOrHideContainer = () => {
|
||||
const {
|
||||
channelIsLoading,
|
||||
filesUploadingForCurrentChannel,
|
||||
files,
|
||||
} = this.props;
|
||||
const {fileSizeWarning, showFileMaxWarning} = this.state;
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
if (
|
||||
!fileSizeWarning && !showFileMaxWarning &&
|
||||
(channelIsLoading || (!files.length && !filesUploadingForCurrentChannel))
|
||||
) {
|
||||
return null;
|
||||
|
||||
if ((channelIsLoading || (!files.length && !filesUploadingForCurrentChannel))) {
|
||||
this.containerRef.current.transition(showFiles, hideFiles, 150, 'ease-out');
|
||||
this.shown = false;
|
||||
} else if (files.length && !this.shown) {
|
||||
this.containerRef.current.transition(hideFiles, showFiles, 350, 'ease-in');
|
||||
this.shown = true;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {fileSizeWarning, showFileMaxWarning} = this.state;
|
||||
const {theme, files} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const fileContainerStyle = {
|
||||
paddingBottom: files.length ? 5 : 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={style.previewContainer}>
|
||||
<View style={style.fileContainer}>
|
||||
<Animatable.View
|
||||
style={[style.fileContainer, fileContainerStyle]}
|
||||
ref={this.containerRef}
|
||||
isInteraction={true}
|
||||
>
|
||||
<ScrollView
|
||||
horizontal={true}
|
||||
style={style.scrollView}
|
||||
@@ -97,21 +159,32 @@ export default class FileUploadPreview extends PureComponent {
|
||||
>
|
||||
{this.buildFilePreviews()}
|
||||
</ScrollView>
|
||||
</View>
|
||||
<View style={style.errorContainer}>
|
||||
{showFileMaxWarning && (
|
||||
<FormattedText
|
||||
style={style.warning}
|
||||
id='mobile.file_upload.max_warning'
|
||||
defaultMessage='Uploads limited to 5 files maximum.'
|
||||
/>
|
||||
)}
|
||||
{Boolean(fileSizeWarning) &&
|
||||
<Text style={style.warning}>
|
||||
{fileSizeWarning}
|
||||
</Text>
|
||||
}
|
||||
</View>
|
||||
</Animatable.View>
|
||||
<Animatable.View
|
||||
ref={this.errorContainerRef}
|
||||
style={style.errorContainer}
|
||||
isInteraction={true}
|
||||
>
|
||||
<Animatable.View
|
||||
ref={this.errorRef}
|
||||
isInteraction={true}
|
||||
style={style.errorTextContainer}
|
||||
useNativeDriver={true}
|
||||
>
|
||||
{showFileMaxWarning && (
|
||||
<FormattedText
|
||||
style={style.warning}
|
||||
id='mobile.file_upload.max_warning'
|
||||
defaultMessage='Uploads limited to 5 files maximum.'
|
||||
/>
|
||||
)}
|
||||
{Boolean(fileSizeWarning) &&
|
||||
<Text style={style.warning}>
|
||||
{fileSizeWarning}
|
||||
</Text>
|
||||
}
|
||||
</Animatable.View>
|
||||
</Animatable.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -119,32 +192,37 @@ export default class FileUploadPreview extends PureComponent {
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
fileContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
errorContainer: {
|
||||
height: 18,
|
||||
},
|
||||
previewContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
fileContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
height: 0,
|
||||
},
|
||||
errorContainer: {
|
||||
height: 0,
|
||||
},
|
||||
errorTextContainer: {
|
||||
marginTop: Platform.select({
|
||||
ios: 4,
|
||||
android: 2,
|
||||
}),
|
||||
marginHorizontal: 12,
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
marginBottom: 10,
|
||||
},
|
||||
scrollViewContent: {
|
||||
alignItems: 'flex-end',
|
||||
marginLeft: 14,
|
||||
paddingRight: 12,
|
||||
},
|
||||
warning: {
|
||||
color: theme.errorTextColor,
|
||||
marginLeft: 14,
|
||||
marginBottom: Platform.select({
|
||||
android: 14,
|
||||
ios: 0,
|
||||
}),
|
||||
flex: 1,
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Platform} from 'react-native';
|
||||
import {View, Platform} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
|
||||
@@ -25,19 +25,22 @@ export default class FileUploadRemove extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
const {theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
style={style.removeButtonWrapper}
|
||||
style={style.tappableContainer}
|
||||
onPress={this.handleOnPress}
|
||||
type={'opacity'}
|
||||
>
|
||||
<Icon
|
||||
name='close-circle'
|
||||
color={this.props.theme.centerChannelColor}
|
||||
size={20}
|
||||
style={style.removeButtonIcon}
|
||||
/>
|
||||
<View style={style.removeButton}>
|
||||
<Icon
|
||||
name='close-circle'
|
||||
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
size={21}
|
||||
style={style.removeIcon}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
@@ -45,25 +48,29 @@ export default class FileUploadRemove extends PureComponent {
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
removeButtonIcon: Platform.select({
|
||||
ios: {
|
||||
marginTop: 2,
|
||||
},
|
||||
}),
|
||||
removeButtonWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
tappableContainer: {
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
elevation: 11,
|
||||
top: 7,
|
||||
right: 7,
|
||||
width: 24,
|
||||
height: 24,
|
||||
top: -5,
|
||||
right: -10,
|
||||
width: 32,
|
||||
height: 32,
|
||||
},
|
||||
removeButton: {
|
||||
borderRadius: 12,
|
||||
alignSelf: 'center',
|
||||
marginTop: Platform.select({
|
||||
ios: 5.4,
|
||||
android: 4.75,
|
||||
}),
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderWidth: 2,
|
||||
borderColor: theme.centerChannelBg,
|
||||
},
|
||||
removeIcon: {
|
||||
position: 'relative',
|
||||
top: Platform.select({
|
||||
ios: 1,
|
||||
android: 0,
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ export default class FileUploadRetry extends PureComponent {
|
||||
>
|
||||
<Icon
|
||||
name='md-refresh'
|
||||
size={50}
|
||||
size={25}
|
||||
color='#fff'
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
@@ -45,5 +45,6 @@ const style = StyleSheet.create({
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 4,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,19 +4,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Text} from 'react-native';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
|
||||
class FormattedTime extends React.PureComponent {
|
||||
export default class FormattedTime extends React.PureComponent {
|
||||
static propTypes = {
|
||||
value: PropTypes.any.isRequired,
|
||||
timeZone: PropTypes.string,
|
||||
children: PropTypes.func,
|
||||
hour12: PropTypes.bool,
|
||||
style: CustomPropTypes.Style,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
getFormattedTime = () => {
|
||||
@@ -24,23 +22,14 @@ class FormattedTime extends React.PureComponent {
|
||||
value,
|
||||
timeZone,
|
||||
hour12,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
const timezoneProps = timeZone ? {timeZone} : {};
|
||||
const options = {
|
||||
...timezoneProps,
|
||||
hour12,
|
||||
};
|
||||
const formattedTime = intl.formatTime(value, options);
|
||||
|
||||
// `formatTime` returns unformatted date string on error like in the case of (react-intl) unsupported timezone.
|
||||
// Therefore, use react-intl by default or moment-timezone for unsupported timezone.
|
||||
if (formattedTime !== String(new Date(value))) {
|
||||
return formattedTime;
|
||||
let format = 'H:mm';
|
||||
if (hour12) {
|
||||
const localeFormat = moment.localeData().longDateFormat('LT');
|
||||
format = localeFormat?.includes('A') ? localeFormat : 'h:mm A';
|
||||
}
|
||||
|
||||
const format = hour12 ? 'hh:mm A' : 'HH:mm';
|
||||
if (timeZone) {
|
||||
return moment.tz(value, timeZone).format(format);
|
||||
}
|
||||
@@ -59,5 +48,3 @@ class FormattedTime extends React.PureComponent {
|
||||
return <Text style={style}>{formattedTime}</Text>;
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(FormattedTime);
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
import React from 'react';
|
||||
import {render} from '@testing-library/react-native';
|
||||
import {IntlProvider} from 'react-intl';
|
||||
import IntlPolyfill from 'intl';
|
||||
import 'intl/locale-data/jsonp/es';
|
||||
import 'intl/locale-data/jsonp/ko';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import FormattedTime from './formatted_time';
|
||||
|
||||
@@ -17,13 +15,11 @@ describe('FormattedTime', () => {
|
||||
hour12: true,
|
||||
};
|
||||
|
||||
setupTest();
|
||||
|
||||
it('should render correctly', () => {
|
||||
console.error = jest.fn();
|
||||
|
||||
let wrapper = renderWithIntl(
|
||||
<FormattedTime {...baseProps}/>
|
||||
<FormattedTime {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.baseElement).toMatchSnapshot();
|
||||
@@ -33,20 +29,22 @@ describe('FormattedTime', () => {
|
||||
<FormattedTime
|
||||
{...baseProps}
|
||||
hour12={false}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getByText('19:02')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should support localization', () => {
|
||||
moment.locale('es');
|
||||
let wrapper = renderWithIntl(
|
||||
<FormattedTime {...baseProps}/>,
|
||||
'es',
|
||||
);
|
||||
|
||||
expect(wrapper.getByText('7:02 p. m.')).toBeTruthy();
|
||||
expect(wrapper.getByText('7:02 PM')).toBeTruthy();
|
||||
|
||||
moment.locale('ko');
|
||||
wrapper = renderWithIntl(
|
||||
<FormattedTime {...baseProps}/>,
|
||||
'ko',
|
||||
@@ -66,6 +64,7 @@ describe('FormattedTime', () => {
|
||||
});
|
||||
|
||||
it('should fallback to default short format for unsupported locale of react-intl ', () => {
|
||||
moment.locale('es');
|
||||
let wrapper = renderWithIntl(
|
||||
<FormattedTime
|
||||
{...baseProps}
|
||||
@@ -74,24 +73,21 @@ describe('FormattedTime', () => {
|
||||
'es',
|
||||
);
|
||||
|
||||
expect(wrapper.getByText('08:47 AM')).toBeTruthy();
|
||||
expect(wrapper.getByText('8:47 AM')).toBeTruthy();
|
||||
|
||||
wrapper = renderWithIntl(
|
||||
<FormattedTime
|
||||
{...baseProps}
|
||||
timeZone='NZ-CHAT'
|
||||
hour12={false}
|
||||
/>
|
||||
/>,
|
||||
'es',
|
||||
);
|
||||
|
||||
expect(wrapper.getByText('08:47')).toBeTruthy();
|
||||
expect(wrapper.getByText('8:47')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
function renderWithIntl(component, locale = 'en') {
|
||||
return render(<IntlProvider locale={locale}>{component}</IntlProvider>);
|
||||
}
|
||||
|
||||
function setupTest() {
|
||||
global.Intl = IntlPolyfill;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import React from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Clipboard,
|
||||
Keyboard,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
@@ -57,7 +58,7 @@ export default class MarkdownCodeBlock extends React.PureComponent {
|
||||
},
|
||||
{
|
||||
language: languageDisplayName,
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
title = intl.formatMessage({
|
||||
@@ -66,7 +67,10 @@ export default class MarkdownCodeBlock extends React.PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
goToScreen(screen, title, passProps);
|
||||
Keyboard.dismiss();
|
||||
requestAnimationFrame(() => {
|
||||
goToScreen(screen, title, passProps);
|
||||
});
|
||||
});
|
||||
|
||||
handleLongPress = async () => {
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('MarkdownEmoji', () => {
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<MarkdownEmoji {...baseProps}/>
|
||||
<MarkdownEmoji {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
|
||||
@@ -12,10 +12,10 @@ import {DeepLinkTypes} from 'app/constants';
|
||||
import {getCurrentServerUrl} from 'app/init/credentials';
|
||||
import mattermostManaged from 'app/mattermost_managed';
|
||||
import BottomSheet from 'app/utils/bottom_sheet';
|
||||
import {alertErrorWithFallback} from 'app/utils/general';
|
||||
import {t} from 'app/utils/i18n';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {matchDeepLink, normalizeProtocol} from 'app/utils/url';
|
||||
import {alertErrorWithFallback} from 'app/utils/general';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
||||
import Config from 'assets/config';
|
||||
|
||||
@@ -58,16 +58,7 @@ export default class MarkdownLink extends PureComponent {
|
||||
|
||||
if (match) {
|
||||
if (match.type === DeepLinkTypes.CHANNEL) {
|
||||
const error = await this.props.actions.handleSelectChannelByName(match.channelName, match.teamName);
|
||||
|
||||
if (error) {
|
||||
const linkFailedMessage = {
|
||||
id: t('mobile.server_link.private_channel.error'),
|
||||
defaultMessage: 'You are not a member of this private channel.',
|
||||
};
|
||||
|
||||
alertErrorWithFallback(this.context.intl, {}, linkFailedMessage);
|
||||
}
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, this.errorBadChannel);
|
||||
} else if (match.type === DeepLinkTypes.PERMALINK) {
|
||||
onPermalinkPress(match.postId, match.teamName);
|
||||
}
|
||||
@@ -92,6 +83,16 @@ export default class MarkdownLink extends PureComponent {
|
||||
}
|
||||
});
|
||||
|
||||
errorBadChannel = () => {
|
||||
const {intl} = this.context;
|
||||
const message = {
|
||||
id: t('mobile.server_link.unreachable_channel.error'),
|
||||
defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.',
|
||||
};
|
||||
|
||||
alertErrorWithFallback(intl, {}, message);
|
||||
};
|
||||
|
||||
parseLinkLiteral = (literal) => {
|
||||
let nextLiteral = literal;
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('MarkdownTable', () => {
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<MarkdownTable {...baseProps}/>
|
||||
<MarkdownTable {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
@@ -40,7 +40,7 @@ describe('MarkdownTable', () => {
|
||||
|
||||
test('should slice rows and columns', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<MarkdownTable {...baseProps}/>
|
||||
<MarkdownTable {...baseProps}/>,
|
||||
);
|
||||
|
||||
const {maxPreviewColumns} = wrapper.state();
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function ActionButtonText({message, style}) {
|
||||
literal={match[0]}
|
||||
emojiName={match[1]}
|
||||
textStyle={style}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
text = text.substring(match[0].length);
|
||||
continue;
|
||||
@@ -48,7 +48,7 @@ export default function ActionButtonText({message, style}) {
|
||||
literal={match[0]}
|
||||
emojiName={emoticonName}
|
||||
textStyle={style}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
text = text.substring(match[0].length);
|
||||
continue;
|
||||
@@ -65,7 +65,7 @@ export default function ActionButtonText({message, style}) {
|
||||
style={style}
|
||||
>
|
||||
{match[0]}
|
||||
</Text>
|
||||
</Text>,
|
||||
);
|
||||
text = text.substring(match[0].length);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export default class AttachmentActions extends PureComponent {
|
||||
options={action.options}
|
||||
postId={postId}
|
||||
disabled={action.disabled}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
case 'button':
|
||||
@@ -55,7 +55,7 @@ export default class AttachmentActions extends PureComponent {
|
||||
name={action.name}
|
||||
postId={postId}
|
||||
disabled={action.disabled}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export default class AttachmentFields extends PureComponent {
|
||||
style={style.field}
|
||||
>
|
||||
{fieldInfos}
|
||||
</View>
|
||||
</View>,
|
||||
);
|
||||
fieldInfos = [];
|
||||
rowPos = 0;
|
||||
@@ -89,7 +89,7 @@ export default class AttachmentFields extends PureComponent {
|
||||
onPermalinkPress={onPermalinkPress}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>,
|
||||
);
|
||||
|
||||
rowPos += 1;
|
||||
@@ -103,7 +103,7 @@ export default class AttachmentFields extends PureComponent {
|
||||
style={style.table}
|
||||
>
|
||||
{fieldInfos}
|
||||
</View>
|
||||
</View>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export default class MessageAttachments extends PureComponent {
|
||||
postId={postId}
|
||||
theme={theme}
|
||||
textStyles={textStyles}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -175,14 +175,14 @@ export default class NetworkIndicator extends PureComponent {
|
||||
this.backgroundColor, {
|
||||
toValue: 1,
|
||||
duration: 100,
|
||||
}
|
||||
},
|
||||
),
|
||||
Animated.timing(
|
||||
this.top, {
|
||||
toValue: (this.getNavBarHeight() - HEIGHT),
|
||||
duration: 300,
|
||||
delay: 500,
|
||||
}
|
||||
},
|
||||
),
|
||||
]).start(() => {
|
||||
this.backgroundColor.setValue(0);
|
||||
@@ -317,7 +317,7 @@ export default class NetworkIndicator extends PureComponent {
|
||||
}),
|
||||
onPress: actions.logout,
|
||||
}],
|
||||
{cancelable: false}
|
||||
{cancelable: false},
|
||||
);
|
||||
closeWebSocket(true);
|
||||
});
|
||||
@@ -340,7 +340,7 @@ export default class NetworkIndicator extends PureComponent {
|
||||
this.top, {
|
||||
toValue: this.getNavBarHeight(),
|
||||
duration: 300,
|
||||
}
|
||||
},
|
||||
).start(() => {
|
||||
this.props.actions.setCurrentUserStatusOffline();
|
||||
});
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PasteableTextInput should render pasteable text input 1`] = `
|
||||
<ConditionalWrapper
|
||||
conditional={true}
|
||||
wrapper={[Function]}
|
||||
<TextInput
|
||||
allowFontScaling={true}
|
||||
onPaste={[MockFunction]}
|
||||
rejectResponderTermination={true}
|
||||
underlineColorAndroid="transparent"
|
||||
>
|
||||
<TextInput
|
||||
allowFontScaling={true}
|
||||
onContentSizeChange={[Function]}
|
||||
onPaste={[MockFunction]}
|
||||
rejectResponderTermination={true}
|
||||
underlineColorAndroid="transparent"
|
||||
>
|
||||
My Text
|
||||
</TextInput>
|
||||
</ConditionalWrapper>
|
||||
My Text
|
||||
</TextInput>
|
||||
`;
|
||||
|
||||
@@ -10,7 +10,7 @@ describe('CustomTextInput', () => {
|
||||
const onPaste = jest.fn();
|
||||
const text = 'My Text';
|
||||
const component = shallow(
|
||||
<CustomTextInput onPaste={onPaste}>{text}</CustomTextInput>
|
||||
<CustomTextInput onPaste={onPaste}>{text}</CustomTextInput>,
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Animated, Easing, NativeEventEmitter, NativeModules, Platform, TextInput} from 'react-native';
|
||||
import {TextInput, NativeEventEmitter, NativeModules} from 'react-native';
|
||||
import CustomTextInput from './custom_text_input';
|
||||
import {ConditionalWrapper} from 'app/components/conditionalWrapper';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
const {OnPasteEventManager} = NativeModules;
|
||||
const OnPasteEventEmitter = new NativeEventEmitter(OnPasteEventManager);
|
||||
@@ -18,10 +16,6 @@ export class PasteableTextInput extends React.PureComponent {
|
||||
forwardRef: PropTypes.any,
|
||||
}
|
||||
|
||||
state = {
|
||||
inputHeight: new Animated.Value(33),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.subscription = OnPasteEventEmitter.addListener('onPaste', this.onPaste);
|
||||
}
|
||||
@@ -37,41 +31,14 @@ export class PasteableTextInput extends React.PureComponent {
|
||||
return onPaste?.(null, event);
|
||||
}
|
||||
|
||||
animateHeight = (event) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
const {height} = event.nativeEvent.contentSize;
|
||||
const {style} = this.props;
|
||||
const {inputHeight} = this.state;
|
||||
const newHeight = Math.min(style.maxHeight, height + ViewTypes.INPUT_VERTICAL_PADDING);
|
||||
const transitionSpeed = height === ViewTypes.INPUT_LINE_HEIGHT ? 500 : 1;
|
||||
|
||||
Animated.timing(inputHeight, {
|
||||
toValue: newHeight,
|
||||
duration: transitionSpeed,
|
||||
easing: Easing.inOut(Easing.sin),
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
wrapperLayout = (children) => {
|
||||
const {inputHeight} = this.state;
|
||||
return <Animated.View style={{flex: 1, height: inputHeight}}>{children}</Animated.View>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {forwardRef, ...props} = this.props;
|
||||
|
||||
return (
|
||||
<ConditionalWrapper
|
||||
conditional={Platform.OS === 'ios'}
|
||||
wrapper={this.wrapperLayout}
|
||||
>
|
||||
<CustomTextInput
|
||||
{...props}
|
||||
ref={forwardRef}
|
||||
onContentSizeChange={this.animateHeight}
|
||||
/>
|
||||
</ConditionalWrapper>
|
||||
<CustomTextInput
|
||||
{...props}
|
||||
ref={forwardRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('PasteableTextInput', () => {
|
||||
const onPaste = jest.fn();
|
||||
const text = 'My Text';
|
||||
const component = shallow(
|
||||
<PasteableTextInput onPaste={onPaste}>{text}</PasteableTextInput>
|
||||
<PasteableTextInput onPaste={onPaste}>{text}</PasteableTextInput>,
|
||||
);
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
@@ -23,7 +23,7 @@ describe('PasteableTextInput', () => {
|
||||
const event = {someData: 'data'};
|
||||
const text = 'My Text';
|
||||
shallow(
|
||||
<PasteableTextInput onPaste={onPaste}>{text}</PasteableTextInput>
|
||||
<PasteableTextInput onPaste={onPaste}>{text}</PasteableTextInput>,
|
||||
);
|
||||
nativeEventEmitter.emit('onPaste', event);
|
||||
expect(onPaste).toHaveBeenCalledWith(null, event);
|
||||
@@ -34,7 +34,7 @@ describe('PasteableTextInput', () => {
|
||||
const onPaste = jest.fn();
|
||||
const text = 'My Text';
|
||||
const component = shallow(
|
||||
<PasteableTextInput onPaste={onPaste}>{text}</PasteableTextInput>
|
||||
<PasteableTextInput onPaste={onPaste}>{text}</PasteableTextInput>,
|
||||
);
|
||||
|
||||
component.instance().subscription.remove = mockRemove;
|
||||
|
||||
@@ -71,7 +71,7 @@ export default class PostAddChannelMember extends React.PureComponent {
|
||||
{
|
||||
username: currentUser.username,
|
||||
addedUsername: usernames[index],
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
actions.sendAddToChannelEphemeralPost(currentUser, usernames[index], message, post.channel_id, post.root_id);
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('PostAttachmentOpenGraph', () => {
|
||||
|
||||
test('should match snapshot, without image and description', () => {
|
||||
const wrapper = shallow(
|
||||
<PostAttachmentOpenGraph {...baseProps}/>
|
||||
<PostAttachmentOpenGraph {...baseProps}/>,
|
||||
);
|
||||
|
||||
// should return null
|
||||
@@ -58,7 +58,7 @@ describe('PostAttachmentOpenGraph', () => {
|
||||
<PostAttachmentOpenGraph
|
||||
{...baseProps}
|
||||
openGraphData={newOpenGraphData}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
@@ -68,14 +68,14 @@ describe('PostAttachmentOpenGraph', () => {
|
||||
<PostAttachmentOpenGraph
|
||||
{...baseProps}
|
||||
openGraphData={{}}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match state and snapshot, on renderImage', () => {
|
||||
const wrapper = shallow(
|
||||
<PostAttachmentOpenGraph {...baseProps}/>
|
||||
<PostAttachmentOpenGraph {...baseProps}/>,
|
||||
);
|
||||
|
||||
// should return null
|
||||
@@ -99,7 +99,7 @@ describe('PostAttachmentOpenGraph', () => {
|
||||
<PostAttachmentOpenGraph
|
||||
{...baseProps}
|
||||
openGraphData={openGraphData}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
|
||||
// should return null
|
||||
@@ -112,7 +112,7 @@ describe('PostAttachmentOpenGraph', () => {
|
||||
|
||||
test('should match result on getFilename', () => {
|
||||
const wrapper = shallow(
|
||||
<PostAttachmentOpenGraph {...baseProps}/>
|
||||
<PostAttachmentOpenGraph {...baseProps}/>,
|
||||
);
|
||||
|
||||
const testCases = [
|
||||
|
||||
@@ -51,7 +51,7 @@ exports[`PostHeader should match snapshot when just a base post 1`] = `
|
||||
John Smith
|
||||
</Text>
|
||||
</TouchableWithFeedbackIOS>
|
||||
<InjectIntl(FormattedTime)
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
@@ -123,7 +123,7 @@ exports[`PostHeader should match snapshot when just a base post in landscape mod
|
||||
John Smith
|
||||
</Text>
|
||||
</TouchableWithFeedbackIOS>
|
||||
<InjectIntl(FormattedTime)
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
@@ -232,7 +232,7 @@ exports[`PostHeader should match snapshot when post is autoresponder 1`] = `
|
||||
}
|
||||
}
|
||||
/>
|
||||
<InjectIntl(FormattedTime)
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
@@ -297,7 +297,7 @@ exports[`PostHeader should match snapshot when post is from system message 1`] =
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<InjectIntl(FormattedTime)
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
@@ -367,7 +367,7 @@ exports[`PostHeader should match snapshot when post is same thread, so dont disp
|
||||
John Smith
|
||||
</Text>
|
||||
</TouchableWithFeedbackIOS>
|
||||
<InjectIntl(FormattedTime)
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
@@ -494,7 +494,7 @@ exports[`PostHeader should match snapshot when post isBot and shouldRenderReplyB
|
||||
}
|
||||
}
|
||||
/>
|
||||
<InjectIntl(FormattedTime)
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
@@ -646,7 +646,7 @@ exports[`PostHeader should match snapshot when post isBot and shouldRenderReplyB
|
||||
}
|
||||
}
|
||||
/>
|
||||
<InjectIntl(FormattedTime)
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
@@ -758,7 +758,7 @@ exports[`PostHeader should match snapshot when post renders Commented On for new
|
||||
John Smith
|
||||
</Text>
|
||||
</TouchableWithFeedbackIOS>
|
||||
<InjectIntl(FormattedTime)
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
@@ -845,7 +845,7 @@ exports[`PostHeader should match snapshot when post should display reply button
|
||||
John Smith
|
||||
</Text>
|
||||
</TouchableWithFeedbackIOS>
|
||||
<InjectIntl(FormattedTime)
|
||||
<FormattedTime
|
||||
hour12={true}
|
||||
style={
|
||||
Object {
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('PostHeader', () => {
|
||||
|
||||
test('should match snapshot when just a base post', () => {
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...baseProps}/>
|
||||
<PostHeader {...baseProps}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find('#ReplyIcon').exists()).toEqual(false);
|
||||
@@ -55,7 +55,7 @@ describe('PostHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...props}/>
|
||||
<PostHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
@@ -67,7 +67,7 @@ describe('PostHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...props}/>
|
||||
<PostHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
@@ -79,7 +79,7 @@ describe('PostHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...props}/>
|
||||
<PostHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find('#ReplyIcon').exists()).toEqual(false);
|
||||
@@ -92,7 +92,7 @@ describe('PostHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...props}/>
|
||||
<PostHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find('#ReplyIcon').exists()).toEqual(false);
|
||||
@@ -108,7 +108,7 @@ describe('PostHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...props}/>
|
||||
<PostHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
@@ -123,7 +123,7 @@ describe('PostHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...props}/>
|
||||
<PostHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
@@ -135,7 +135,7 @@ describe('PostHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...props}/>
|
||||
<PostHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find('#ReplyIcon').exists()).toEqual(false);
|
||||
@@ -150,7 +150,7 @@ describe('PostHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostHeader {...props}/>
|
||||
<PostHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('PostPreHeader', () => {
|
||||
|
||||
test('should match snapshot when not flagged or pinned post', () => {
|
||||
const wrapper = shallow(
|
||||
<PostPreHeader {...baseProps}/>
|
||||
<PostPreHeader {...baseProps}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.type()).toBeNull();
|
||||
@@ -35,7 +35,7 @@ describe('PostPreHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostPreHeader {...props}/>
|
||||
<PostPreHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.type()).toBeNull();
|
||||
@@ -49,7 +49,7 @@ describe('PostPreHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostPreHeader {...props}/>
|
||||
<PostPreHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.type()).toBeNull();
|
||||
@@ -62,7 +62,7 @@ describe('PostPreHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostPreHeader {...props}/>
|
||||
<PostPreHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find('#flagIcon').exists()).toEqual(true);
|
||||
@@ -77,7 +77,7 @@ describe('PostPreHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostPreHeader {...props}/>
|
||||
<PostPreHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find('#flagIcon').exists()).toEqual(false);
|
||||
@@ -93,7 +93,7 @@ describe('PostPreHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostPreHeader {...props}/>
|
||||
<PostPreHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find('#flagIcon').exists()).toEqual(true);
|
||||
@@ -110,7 +110,7 @@ describe('PostPreHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostPreHeader {...props}/>
|
||||
<PostPreHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find('#flagIcon').exists()).toEqual(true);
|
||||
@@ -127,7 +127,7 @@ describe('PostPreHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostPreHeader {...props}/>
|
||||
<PostPreHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.find('#flagIcon').exists()).toEqual(false);
|
||||
@@ -145,7 +145,7 @@ describe('PostPreHeader', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostPreHeader {...props}/>
|
||||
<PostPreHeader {...props}/>,
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.type()).toBeNull();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {memo} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
View,
|
||||
@@ -71,4 +71,4 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
export default NewMessagesDivider;
|
||||
export default memo(NewMessagesDivider);
|
||||
|
||||
@@ -18,6 +18,8 @@ import {changeOpacity} from 'app/utils/theme';
|
||||
import {matchDeepLink} from 'app/utils/url';
|
||||
import telemetry from 'app/telemetry';
|
||||
import {showModalOverCurrentContext} from 'app/actions/navigation';
|
||||
import {alertErrorWithFallback} from 'app/utils/general';
|
||||
import {t} from 'app/utils/i18n';
|
||||
|
||||
import DateHeader from './date_header';
|
||||
import NewMessagesDivider from './new_messages_divider';
|
||||
@@ -165,12 +167,14 @@ export default class PostList extends PureComponent {
|
||||
};
|
||||
|
||||
handleContentSizeChange = (contentWidth, contentHeight) => {
|
||||
this.setState({contentHeight}, () => {
|
||||
if (this.state.postListHeight && contentHeight < this.state.postListHeight && this.props.extraData) {
|
||||
// We still have less than 1 screen of posts loaded with more to get, so load more
|
||||
this.props.onLoadMoreUp();
|
||||
}
|
||||
});
|
||||
if (this.state.contentHeight !== contentHeight) {
|
||||
this.setState({contentHeight}, () => {
|
||||
if (this.state.postListHeight && contentHeight < this.state.postListHeight && this.props.extraData) {
|
||||
// We still have less than 1 screen of posts loaded with more to get, so load more
|
||||
this.props.onLoadMoreUp();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleDeepLink = (url) => {
|
||||
@@ -180,7 +184,7 @@ export default class PostList extends PureComponent {
|
||||
|
||||
if (match) {
|
||||
if (match.type === DeepLinkTypes.CHANNEL) {
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName);
|
||||
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, this.errorBadChannel);
|
||||
} else if (match.type === DeepLinkTypes.PERMALINK) {
|
||||
this.handlePermalinkPress(match.postId, match.teamName);
|
||||
}
|
||||
@@ -201,7 +205,29 @@ export default class PostList extends PureComponent {
|
||||
|
||||
handleLayout = (event) => {
|
||||
const {height} = event.nativeEvent.layout;
|
||||
this.setState({postListHeight: height});
|
||||
if (this.state.postListHeight !== height) {
|
||||
this.setState({postListHeight: height});
|
||||
}
|
||||
};
|
||||
|
||||
errorBadTeam = () => {
|
||||
const {intl} = this.context;
|
||||
const message = {
|
||||
id: t('mobile.server_link.unreachable_team.error'),
|
||||
defaultMessage: 'This link belongs to a deleted team or to a team to which you do not have access.',
|
||||
};
|
||||
|
||||
alertErrorWithFallback(intl, {}, message);
|
||||
};
|
||||
|
||||
errorBadChannel = () => {
|
||||
const {intl} = this.context;
|
||||
const message = {
|
||||
id: t('mobile.server_link.unreachable_channel.error'),
|
||||
defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.',
|
||||
};
|
||||
|
||||
alertErrorWithFallback(intl, {}, message);
|
||||
};
|
||||
|
||||
handlePermalinkPress = (postId, teamName) => {
|
||||
@@ -211,7 +237,7 @@ export default class PostList extends PureComponent {
|
||||
if (onPermalinkPress) {
|
||||
onPermalinkPress(postId, true);
|
||||
} else {
|
||||
actions.loadChannelsByTeamName(teamName);
|
||||
actions.loadChannelsByTeamName(teamName, this.errorBadTeam);
|
||||
this.showPermalinkView(postId);
|
||||
}
|
||||
};
|
||||
@@ -377,7 +403,9 @@ export default class PostList extends PureComponent {
|
||||
resetPostList = () => {
|
||||
this.contentOffsetY = 0;
|
||||
this.hasDoneInitialScroll = false;
|
||||
this.setState({contentHeight: 0});
|
||||
if (this.state.contentHeight !== 0) {
|
||||
this.setState({contentHeight: 0});
|
||||
}
|
||||
}
|
||||
|
||||
scrollToIndex = (index) => {
|
||||
@@ -401,7 +429,7 @@ export default class PostList extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
showPermalinkView = (postId) => {
|
||||
showPermalinkView = (postId, error = '') => {
|
||||
const {actions} = this.props;
|
||||
|
||||
actions.selectFocusedPostId(postId);
|
||||
@@ -411,6 +439,7 @@ export default class PostList extends PureComponent {
|
||||
const passProps = {
|
||||
isPermalink: true,
|
||||
onClose: this.handleClosePermalink,
|
||||
error,
|
||||
};
|
||||
const options = {
|
||||
layout: {
|
||||
|
||||
@@ -14,7 +14,7 @@ jest.mock('react-intl');
|
||||
|
||||
describe('PostList', () => {
|
||||
const serverURL = 'https://server-url.fake';
|
||||
const deeplinkRoot = 'mattermost-beta://server-url.fake';
|
||||
const deeplinkRoot = 'mattermost://server-url.fake';
|
||||
|
||||
const baseProps = {
|
||||
actions: {
|
||||
@@ -40,7 +40,7 @@ describe('PostList', () => {
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<PostList {...baseProps}/>
|
||||
<PostList {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
@@ -49,7 +49,7 @@ describe('PostList', () => {
|
||||
test('setting permalink deep link', () => {
|
||||
const showModalOverCurrentContext = jest.spyOn(NavigationActions, 'showModalOverCurrentContext');
|
||||
const wrapper = shallow(
|
||||
<PostList {...baseProps}/>
|
||||
<PostList {...baseProps}/>,
|
||||
);
|
||||
|
||||
wrapper.setProps({deepLinkURL: deepLinks.permalink});
|
||||
@@ -61,7 +61,7 @@ describe('PostList', () => {
|
||||
|
||||
test('setting channel deep link', () => {
|
||||
const wrapper = shallow(
|
||||
<PostList {...baseProps}/>
|
||||
<PostList {...baseProps}/>,
|
||||
);
|
||||
|
||||
wrapper.setProps({deepLinkURL: deepLinks.channel});
|
||||
@@ -74,7 +74,7 @@ describe('PostList', () => {
|
||||
jest.spyOn(global, 'requestAnimationFrame').mockImplementation((cb) => cb());
|
||||
|
||||
const wrapper = shallow(
|
||||
<PostList {...baseProps}/>
|
||||
<PostList {...baseProps}/>,
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
const flatListScrollToIndex = jest.spyOn(instance, 'flatListScrollToIndex');
|
||||
@@ -103,7 +103,7 @@ describe('PostList', () => {
|
||||
|
||||
test('should load more posts if available space on the screen', () => {
|
||||
const wrapper = shallow(
|
||||
<PostList {...baseProps}/>
|
||||
<PostList {...baseProps}/>,
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
instance.loadToFillContent = jest.fn();
|
||||
|
||||
@@ -14,7 +14,7 @@ exports[`PostTextBox should match, full snapshot 1`] = `
|
||||
"borderTopWidth": 1,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "center",
|
||||
"paddingVertical": 4,
|
||||
"paddingBottom": 2,
|
||||
},
|
||||
null,
|
||||
]
|
||||
@@ -24,6 +24,7 @@ exports[`PostTextBox should match, full snapshot 1`] = `
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"alignItems": "stretch",
|
||||
"paddingTop": 7,
|
||||
}
|
||||
}
|
||||
disableScrollViewPanResponder={true}
|
||||
@@ -36,11 +37,8 @@ exports[`PostTextBox should match, full snapshot 1`] = `
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
"flexDirection": "column",
|
||||
"marginLeft": 10,
|
||||
"marginRight": 10,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -53,6 +51,7 @@ exports[`PostTextBox should match, full snapshot 1`] = `
|
||||
keyboardType="default"
|
||||
multiline={true}
|
||||
onChangeText={[Function]}
|
||||
onContentSizeChange={null}
|
||||
onEndEditing={[Function]}
|
||||
onPaste={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
@@ -61,100 +60,250 @@ exports[`PostTextBox should match, full snapshot 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 14,
|
||||
"fontSize": 15,
|
||||
"lineHeight": 20,
|
||||
"maxHeight": 150,
|
||||
"paddingBottom": 8,
|
||||
"paddingLeft": 12,
|
||||
"paddingRight": 12,
|
||||
"paddingTop": 8,
|
||||
"minHeight": 30,
|
||||
"paddingBottom": 6,
|
||||
"paddingHorizontal": 12,
|
||||
"paddingTop": 6,
|
||||
}
|
||||
}
|
||||
underlineColorAndroid="transparent"
|
||||
value=""
|
||||
/>
|
||||
<Connect(FileUploadPreview)
|
||||
files={Array []}
|
||||
rootId=""
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"display": "flex",
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "space-between",
|
||||
}
|
||||
}
|
||||
>
|
||||
<React.Fragment>
|
||||
<Connect(FileUploadPreview)
|
||||
files={Array []}
|
||||
rootId=""
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"display": "flex",
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "space-between",
|
||||
"paddingBottom": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.2}
|
||||
disabled={false}
|
||||
onPress={[Function]}
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"paddingLeft": 10,
|
||||
"paddingRight": 10,
|
||||
"display": "flex",
|
||||
"flexDirection": "row",
|
||||
"height": 44,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
color="#3d3c40"
|
||||
name="at"
|
||||
size={20}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.2}
|
||||
disabled={false}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"paddingLeft": 10,
|
||||
"paddingRight": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Image
|
||||
source={
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.2}
|
||||
disabled={false}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"testUri": "../../../dist/assets/images/icons/slash-forward-box.png",
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
color="rgba(61,60,64,0.64)"
|
||||
name="at"
|
||||
size={24}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.2}
|
||||
disabled={false}
|
||||
onPress={[Function]}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Image
|
||||
source={
|
||||
Object {
|
||||
"height": 20,
|
||||
"opacity": 1,
|
||||
"tintColor": "#3d3c40",
|
||||
"width": 20,
|
||||
},
|
||||
]
|
||||
"testUri": "../../../dist/assets/images/icons/slash-forward-box.png",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"height": 24,
|
||||
"opacity": 1,
|
||||
"tintColor": "rgba(61,60,64,0.64)",
|
||||
"width": 24,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<FileUploadButton
|
||||
blurTextBox={[Function]}
|
||||
browseFileTypes="public.item"
|
||||
buttonContainerStyle={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
canBrowseFiles={true}
|
||||
canBrowsePhotoLibrary={true}
|
||||
canBrowseVideoLibrary={true}
|
||||
canTakePhoto={true}
|
||||
canTakeVideo={true}
|
||||
extraOptions={null}
|
||||
fileCount={0}
|
||||
maxFileCount={5}
|
||||
maxFileSize={1024}
|
||||
onShowFileMaxWarning={[Function]}
|
||||
onShowFileSizeWarning={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
uploadFiles={[Function]}
|
||||
validMimeTypes={Array []}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<FileUploadButton
|
||||
blurTextBox={[Function]}
|
||||
browseFileTypes="public.item"
|
||||
canBrowseFiles={true}
|
||||
canBrowsePhotoLibrary={true}
|
||||
canBrowseVideoLibrary={true}
|
||||
canTakePhoto={true}
|
||||
canTakeVideo={true}
|
||||
extraOptions={null}
|
||||
fileCount={0}
|
||||
maxFileCount={5}
|
||||
maxFileSize={1024}
|
||||
onShowFileMaxWarning={[Function]}
|
||||
onShowFileSizeWarning={[Function]}
|
||||
<ImageUploadButton
|
||||
blurTextBox={[Function]}
|
||||
browseFileTypes="public.item"
|
||||
buttonContainerStyle={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
canBrowseFiles={true}
|
||||
canBrowsePhotoLibrary={true}
|
||||
canBrowseVideoLibrary={true}
|
||||
canTakePhoto={true}
|
||||
canTakeVideo={true}
|
||||
extraOptions={null}
|
||||
fileCount={0}
|
||||
maxFileCount={5}
|
||||
maxFileSize={1024}
|
||||
onShowFileMaxWarning={[Function]}
|
||||
onShowFileSizeWarning={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
uploadFiles={[Function]}
|
||||
validMimeTypes={Array []}
|
||||
/>
|
||||
<AttachmentButton
|
||||
blurTextBox={[Function]}
|
||||
buttonContainerStyle={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
canTakePhoto={true}
|
||||
canTakeVideo={true}
|
||||
fileCount={0}
|
||||
maxFileCount={5}
|
||||
maxFileSize={1024}
|
||||
onShowFileMaxWarning={[Function]}
|
||||
onShowFileSizeWarning={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
uploadFiles={[Function]}
|
||||
validMimeTypes={Array []}
|
||||
/>
|
||||
</View>
|
||||
<SendButton
|
||||
disabled={true}
|
||||
handleSendMessage={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
@@ -184,131 +333,9 @@ exports[`PostTextBox should match, full snapshot 1`] = `
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
uploadFiles={[Function]}
|
||||
validMimeTypes={Array []}
|
||||
/>
|
||||
<ImageUploadButton
|
||||
blurTextBox={[Function]}
|
||||
browseFileTypes="public.item"
|
||||
canBrowseFiles={true}
|
||||
canBrowsePhotoLibrary={true}
|
||||
canBrowseVideoLibrary={true}
|
||||
canTakePhoto={true}
|
||||
canTakeVideo={true}
|
||||
extraOptions={null}
|
||||
fileCount={0}
|
||||
maxFileCount={5}
|
||||
maxFileSize={1024}
|
||||
onShowFileMaxWarning={[Function]}
|
||||
onShowFileSizeWarning={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
uploadFiles={[Function]}
|
||||
validMimeTypes={Array []}
|
||||
/>
|
||||
<AttachmentButton
|
||||
blurTextBox={[Function]}
|
||||
canTakePhoto={true}
|
||||
canTakeVideo={true}
|
||||
fileCount={0}
|
||||
maxFileCount={5}
|
||||
maxFileSize={1024}
|
||||
onShowFileMaxWarning={[Function]}
|
||||
onShowFileSizeWarning={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
uploadFiles={[Function]}
|
||||
validMimeTypes={Array []}
|
||||
/>
|
||||
</View>
|
||||
<SendButton
|
||||
disabled={true}
|
||||
handleSendMessage={[Function]}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CameraButton should match snapshot 1`] = `
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
style={Object {}}
|
||||
type="opacity"
|
||||
>
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
color="rgba(61,60,64,0.64)"
|
||||
name="camera-outline"
|
||||
size={24}
|
||||
/>
|
||||
</TouchableWithFeedbackIOS>
|
||||
`;
|
||||
@@ -0,0 +1,16 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileUploadButton should match snapshot 1`] = `
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
style={Object {}}
|
||||
type="opacity"
|
||||
>
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
color="rgba(61,60,64,0.64)"
|
||||
name="file-document-outline"
|
||||
size={24}
|
||||
/>
|
||||
</TouchableWithFeedbackIOS>
|
||||
`;
|
||||
@@ -0,0 +1,16 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ImageUploadButton should match snapshot 1`] = `
|
||||
<TouchableWithFeedbackIOS
|
||||
onPress={[Function]}
|
||||
style={Object {}}
|
||||
type="opacity"
|
||||
>
|
||||
<Icon
|
||||
allowFontScaling={false}
|
||||
color="rgba(61,60,64,0.64)"
|
||||
name="image-outline"
|
||||
size={24}
|
||||
/>
|
||||
</TouchableWithFeedbackIOS>
|
||||
`;
|
||||
@@ -6,31 +6,26 @@ import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import {ICON_SIZE} from 'app/constants/post_textbox';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {PermissionTypes} from 'app/constants';
|
||||
|
||||
export default class AttachmentButton extends PureComponent {
|
||||
static propTypes = {
|
||||
validMimeTypes: PropTypes.array,
|
||||
fileCount: PropTypes.number,
|
||||
maxFileCount: PropTypes.number.isRequired,
|
||||
maxFileSize: PropTypes.number.isRequired,
|
||||
onShowFileMaxWarning: PropTypes.func,
|
||||
onShowFileSizeWarning: PropTypes.func,
|
||||
onShowUnsupportedMimeTypeWarning: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadFiles: PropTypes.func.isRequired,
|
||||
buttonContainerStyle: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -102,106 +97,70 @@ export default class AttachmentButton extends PureComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadFiles([response]);
|
||||
this.props.uploadFiles([response]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
hasCameraPermission = async () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
const {formatMessage} = this.context.intl;
|
||||
let permissionRequest;
|
||||
const targetSource = 'camera';
|
||||
const hasPermissionToStorage = await Permissions.check(targetSource);
|
||||
const {formatMessage} = this.context.intl;
|
||||
const targetSource = Platform.OS === 'ios' ?
|
||||
Permissions.PERMISSIONS.IOS.CAMERA :
|
||||
Permissions.PERMISSIONS.ANDROID.CAMERA;
|
||||
const hasPermission = await Permissions.check(targetSource);
|
||||
|
||||
switch (hasPermissionToStorage) {
|
||||
case PermissionTypes.UNDETERMINED:
|
||||
permissionRequest = await Permissions.request(targetSource);
|
||||
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case PermissionTypes.DENIED: {
|
||||
const canOpenSettings = await Permissions.canOpenSettings();
|
||||
let grantOption = null;
|
||||
if (canOpenSettings) {
|
||||
grantOption = {
|
||||
switch (hasPermission) {
|
||||
case Permissions.RESULTS.DENIED:
|
||||
case Permissions.RESULTS.UNAVAILABLE: {
|
||||
const permissionRequest = await Permissions.request(targetSource);
|
||||
|
||||
return permissionRequest === Permissions.RESULTS.GRANTED;
|
||||
}
|
||||
case Permissions.RESULTS.BLOCKED: {
|
||||
const grantOption = {
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
onPress: () => Permissions.openSettings(),
|
||||
};
|
||||
|
||||
const {title, text} = this.getPermissionDeniedMessage();
|
||||
|
||||
Alert.alert(
|
||||
title,
|
||||
text,
|
||||
[
|
||||
grantOption,
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
id: 'mobile.permission_denied_dismiss',
|
||||
defaultMessage: 'Don\'t Allow',
|
||||
}),
|
||||
onPress: () => Permissions.openSettings(),
|
||||
};
|
||||
}
|
||||
|
||||
const {title, text} = this.getPermissionDeniedMessage();
|
||||
|
||||
Alert.alert(
|
||||
title,
|
||||
text,
|
||||
[
|
||||
grantOption,
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_dismiss',
|
||||
defaultMessage: 'Don\'t Allow',
|
||||
}),
|
||||
},
|
||||
],
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
uploadFiles = async (files) => {
|
||||
const file = files[0];
|
||||
if (!file.fileSize | !file.fileName) {
|
||||
const path = (file.path || file.uri).replace('file://', '');
|
||||
const fileInfo = await RNFetchBlob.fs.stat(path);
|
||||
file.fileSize = fileInfo.size;
|
||||
file.fileName = fileInfo.filename;
|
||||
}
|
||||
|
||||
if (!file.type) {
|
||||
file.type = lookupMimeType(file.fileName);
|
||||
}
|
||||
|
||||
const {validMimeTypes} = this.props;
|
||||
if (validMimeTypes.length && !validMimeTypes.includes(file.type)) {
|
||||
this.props.onShowUnsupportedMimeTypeWarning();
|
||||
} else if (file.fileSize > this.props.maxFileSize) {
|
||||
this.props.onShowFileSizeWarning(file.fileName);
|
||||
} else {
|
||||
this.props.uploadFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
|
||||
const {theme, buttonContainerStyle} = this.props;
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={this.attachFileFromCamera}
|
||||
style={style.buttonContainer}
|
||||
style={buttonContainerStyle}
|
||||
type={'opacity'}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
color={theme.centerChannelColor}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
name='camera-outline'
|
||||
size={20}
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
buttonContainer: {
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
});
|
||||
103
app/components/post_textbox/components/camera_button.test.js
Normal file
103
app/components/post_textbox/components/camera_button.test.js
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Alert, Platform} from 'react-native';
|
||||
import {shallow} from 'enzyme';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import CameraButton from './camera_button';
|
||||
|
||||
jest.mock('react-intl');
|
||||
jest.mock('react-native-image-picker', () => ({
|
||||
launchCamera: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('CameraButton', () => {
|
||||
const formatMessage = jest.fn();
|
||||
const baseProps = {
|
||||
fileCount: 0,
|
||||
maxFileCount: 5,
|
||||
onShowFileMaxWarning: jest.fn(),
|
||||
theme: Preferences.THEMES.default,
|
||||
uploadFiles: jest.fn(),
|
||||
buttonContainerStyle: {},
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<CameraButton {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should return permission false if permission is denied in iOS', async () => {
|
||||
jest.spyOn(Permissions, 'check').mockReturnValue(Permissions.RESULTS.UNAVAILABLE);
|
||||
jest.spyOn(Permissions, 'request').mockReturnValue(Permissions.RESULTS.DENIED);
|
||||
|
||||
const wrapper = shallow(
|
||||
<CameraButton {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
const hasPermission = await wrapper.instance().hasCameraPermission();
|
||||
expect(Permissions.check).toHaveBeenCalled();
|
||||
expect(Permissions.request).toHaveBeenCalled();
|
||||
expect(Alert.alert).not.toHaveBeenCalled();
|
||||
expect(hasPermission).toBe(false);
|
||||
});
|
||||
|
||||
test('should show permission denied alert and return permission false if permission is blocked in iOS', async () => {
|
||||
jest.spyOn(Permissions, 'check').mockReturnValue(Permissions.RESULTS.BLOCKED);
|
||||
jest.spyOn(Alert, 'alert').mockReturnValue(true);
|
||||
|
||||
const wrapper = shallow(
|
||||
<CameraButton {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
const hasPermission = await wrapper.instance().hasCameraPermission();
|
||||
expect(Permissions.check).toHaveBeenCalled();
|
||||
expect(Permissions.request).not.toHaveBeenCalled();
|
||||
expect(Alert.alert).toHaveBeenCalled();
|
||||
expect(hasPermission).toBe(false);
|
||||
});
|
||||
|
||||
test('hasCameraPermission returns true when permission has been granted', async () => {
|
||||
const platformPermissions = [{
|
||||
platform: 'ios',
|
||||
permission: Permissions.PERMISSIONS.IOS.CAMERA,
|
||||
}, {
|
||||
platform: 'android',
|
||||
permission: Permissions.PERMISSIONS.ANDROID.CAMERA,
|
||||
}];
|
||||
|
||||
for (let i = 0; i < platformPermissions.length; i++) {
|
||||
const {platform, permission} = platformPermissions[i];
|
||||
Platform.OS = platform;
|
||||
|
||||
const check = jest.spyOn(Permissions, 'check');
|
||||
const request = jest.spyOn(Permissions, 'request');
|
||||
request.mockReturnValue(Permissions.RESULTS.GRANTED);
|
||||
|
||||
const wrapper = shallow(
|
||||
<CameraButton {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
|
||||
check.mockReturnValueOnce(Permissions.RESULTS.DENIED);
|
||||
let hasPermission = await instance.hasCameraPermission(); // eslint-disable-line no-await-in-loop
|
||||
expect(check).toHaveBeenCalledWith(permission);
|
||||
expect(request).toHaveBeenCalled();
|
||||
expect(hasPermission).toBe(true);
|
||||
|
||||
check.mockReturnValueOnce(Permissions.RESULTS.UNAVAILABLE);
|
||||
hasPermission = await instance.hasCameraPermission(); // eslint-disable-line no-await-in-loop
|
||||
expect(check).toHaveBeenCalledWith(permission);
|
||||
expect(request).toHaveBeenCalled();
|
||||
expect(hasPermission).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -7,20 +7,18 @@ import {
|
||||
Alert,
|
||||
NativeModules,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import {ICON_SIZE} from 'app/constants/post_textbox';
|
||||
import AndroidOpenSettings from 'react-native-android-open-settings';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import DocumentPicker from 'react-native-document-picker';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {PermissionTypes} from 'app/constants';
|
||||
|
||||
const ShareExtension = NativeModules.MattermostShare;
|
||||
|
||||
@@ -28,15 +26,12 @@ export default class FileUploadButton extends PureComponent {
|
||||
static propTypes = {
|
||||
blurTextBox: PropTypes.func.isRequired,
|
||||
browseFileTypes: PropTypes.string,
|
||||
validMimeTypes: PropTypes.array,
|
||||
fileCount: PropTypes.number,
|
||||
maxFileCount: PropTypes.number.isRequired,
|
||||
maxFileSize: PropTypes.number.isRequired,
|
||||
onShowFileMaxWarning: PropTypes.func,
|
||||
onShowFileSizeWarning: PropTypes.func,
|
||||
onShowUnsupportedMimeTypeWarning: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadFiles: PropTypes.func.isRequired,
|
||||
buttonContainerStyle: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -90,7 +85,7 @@ export default class FileUploadButton extends PureComponent {
|
||||
// Decode file uri to get the actual path
|
||||
res.uri = decodeURIComponent(res.uri);
|
||||
|
||||
this.uploadFiles([res]);
|
||||
this.props.uploadFiles([res]);
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
@@ -100,17 +95,17 @@ export default class FileUploadButton extends PureComponent {
|
||||
hasStoragePermission = async () => {
|
||||
if (Platform.OS === 'android') {
|
||||
const {formatMessage} = this.context.intl;
|
||||
let permissionRequest;
|
||||
const hasPermissionToStorage = await Permissions.check('storage');
|
||||
const storagePermission = Permissions.PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE;
|
||||
const hasPermissionToStorage = await Permissions.check(storagePermission);
|
||||
|
||||
switch (hasPermissionToStorage) {
|
||||
case PermissionTypes.UNDETERMINED:
|
||||
permissionRequest = await Permissions.request('storage');
|
||||
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case PermissionTypes.DENIED: {
|
||||
case Permissions.RESULTS.DENIED:
|
||||
case Permissions.RESULTS.UNAVAILABLE: {
|
||||
const permissionRequest = await Permissions.request(storagePermission);
|
||||
|
||||
return permissionRequest === Permissions.RESULTS.GRANTED;
|
||||
}
|
||||
case Permissions.RESULTS.BLOCKED: {
|
||||
const {title, text} = this.getPermissionDeniedMessage();
|
||||
|
||||
Alert.alert(
|
||||
@@ -130,7 +125,7 @@ export default class FileUploadButton extends PureComponent {
|
||||
}),
|
||||
onPress: () => AndroidOpenSettings.appDetailsSettings(),
|
||||
},
|
||||
]
|
||||
],
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@@ -140,29 +135,6 @@ export default class FileUploadButton extends PureComponent {
|
||||
return true;
|
||||
};
|
||||
|
||||
uploadFiles = async (files) => {
|
||||
const file = files[0];
|
||||
if (!file.fileSize | !file.fileName) {
|
||||
const path = (file.path || file.uri).replace('file://', '');
|
||||
const fileInfo = await RNFetchBlob.fs.stat(path);
|
||||
file.fileSize = fileInfo.size;
|
||||
file.fileName = fileInfo.filename;
|
||||
}
|
||||
|
||||
if (!file.type) {
|
||||
file.type = lookupMimeType(file.fileName);
|
||||
}
|
||||
|
||||
const {validMimeTypes} = this.props;
|
||||
if (validMimeTypes.length && !validMimeTypes.includes(file.type)) {
|
||||
this.props.onShowUnsupportedMimeTypeWarning();
|
||||
} else if (file.fileSize > this.props.maxFileSize) {
|
||||
this.props.onShowFileSizeWarning(file.fileName);
|
||||
} else {
|
||||
this.props.uploadFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
handleButtonPress = () => {
|
||||
const {
|
||||
fileCount,
|
||||
@@ -179,26 +151,19 @@ export default class FileUploadButton extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
const {theme, buttonContainerStyle} = this.props;
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={this.handleButtonPress}
|
||||
style={style.buttonContainer}
|
||||
style={buttonContainerStyle}
|
||||
type={'opacity'}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
color={theme.centerChannelColor}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
name='file-document-outline'
|
||||
size={20}
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
buttonContainer: {
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Alert, Platform} from 'react-native';
|
||||
import {shallow} from 'enzyme';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import FileUploadButton from './file_upload_button';
|
||||
|
||||
jest.mock('react-intl');
|
||||
jest.mock('react-native-image-picker', () => ({
|
||||
launchCamera: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('FileUploadButton', () => {
|
||||
const formatMessage = jest.fn();
|
||||
const baseProps = {
|
||||
blurTextBox: jest.fn(),
|
||||
browseFileTypes: '*',
|
||||
fileCount: 0,
|
||||
maxFileCount: 5,
|
||||
onShowFileMaxWarning: jest.fn(),
|
||||
theme: Preferences.THEMES.default,
|
||||
uploadFiles: jest.fn(),
|
||||
buttonContainerStyle: {},
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
Platform.OS = 'android';
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Platform.OS = 'ios';
|
||||
});
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<FileUploadButton {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should return permission false if permission is denied in Android', async () => {
|
||||
jest.spyOn(Permissions, 'check').mockReturnValue(Permissions.RESULTS.UNAVAILABLE);
|
||||
jest.spyOn(Permissions, 'request').mockReturnValue(Permissions.RESULTS.DENIED);
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileUploadButton {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
const hasPermission = await wrapper.instance().hasStoragePermission();
|
||||
expect(Permissions.check).toHaveBeenCalled();
|
||||
expect(Permissions.request).toHaveBeenCalled();
|
||||
expect(Alert.alert).not.toHaveBeenCalled();
|
||||
expect(hasPermission).toBe(false);
|
||||
});
|
||||
|
||||
test('should show permission denied alert and return permission false if permission is blocked in Android', async () => {
|
||||
jest.spyOn(Permissions, 'check').mockReturnValue(Permissions.RESULTS.BLOCKED);
|
||||
jest.spyOn(Alert, 'alert').mockReturnValue(true);
|
||||
|
||||
const wrapper = shallow(
|
||||
<FileUploadButton {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
const hasPermission = await wrapper.instance().hasStoragePermission();
|
||||
expect(Permissions.check).toHaveBeenCalled();
|
||||
expect(Permissions.request).not.toHaveBeenCalled();
|
||||
expect(Alert.alert).toHaveBeenCalled();
|
||||
expect(hasPermission).toBe(false);
|
||||
});
|
||||
|
||||
test('hasStoragePermission returns true when permission has been granted', async () => {
|
||||
const wrapper = shallow(
|
||||
<FileUploadButton {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
const check = jest.spyOn(Permissions, 'check');
|
||||
const request = jest.spyOn(Permissions, 'request');
|
||||
|
||||
// On iOS storage permissions are not checked
|
||||
Platform.OS = 'ios';
|
||||
let hasPermission = await instance.hasStoragePermission();
|
||||
expect(check).not.toHaveBeenCalled();
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
expect(hasPermission).toBe(true);
|
||||
|
||||
Platform.OS = 'android';
|
||||
request.mockReturnValue(Permissions.RESULTS.GRANTED);
|
||||
const permission = Permissions.PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE;
|
||||
|
||||
check.mockReturnValueOnce(Permissions.RESULTS.DENIED);
|
||||
hasPermission = await instance.hasStoragePermission();
|
||||
expect(check).toHaveBeenCalledWith(permission);
|
||||
expect(request).toHaveBeenCalled();
|
||||
expect(hasPermission).toBe(true);
|
||||
|
||||
check.mockReturnValueOnce(Permissions.RESULTS.UNAVAILABLE);
|
||||
hasPermission = await instance.hasStoragePermission();
|
||||
expect(check).toHaveBeenCalledWith(permission);
|
||||
expect(request).toHaveBeenCalled();
|
||||
expect(hasPermission).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -6,32 +6,27 @@ import {intlShape} from 'react-intl';
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import {ICON_SIZE} from 'app/constants/post_textbox';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import {lookupMimeType} from 'mattermost-redux/utils/file_utils';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
import {PermissionTypes} from 'app/constants';
|
||||
|
||||
export default class ImageUploadButton extends PureComponent {
|
||||
static propTypes = {
|
||||
blurTextBox: PropTypes.func.isRequired,
|
||||
validMimeTypes: PropTypes.array,
|
||||
fileCount: PropTypes.number,
|
||||
maxFileCount: PropTypes.number.isRequired,
|
||||
maxFileSize: PropTypes.number.isRequired,
|
||||
onShowFileMaxWarning: PropTypes.func,
|
||||
onShowFileSizeWarning: PropTypes.func,
|
||||
onShowUnsupportedMimeTypeWarning: PropTypes.func,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadFiles: PropTypes.func.isRequired,
|
||||
buttonContainerStyle: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -104,84 +99,56 @@ export default class ImageUploadButton extends PureComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadFiles([response]);
|
||||
this.props.uploadFiles([response]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
hasPhotoPermission = async () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
const {formatMessage} = this.context.intl;
|
||||
let permissionRequest;
|
||||
const targetSource = 'photo';
|
||||
const hasPermissionToStorage = await Permissions.check(targetSource);
|
||||
const {formatMessage} = this.context.intl;
|
||||
const targetSource = Platform.OS === 'ios' ?
|
||||
Permissions.PERMISSIONS.IOS.PHOTO_LIBRARY :
|
||||
Permissions.PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE;
|
||||
const hasPermission = await Permissions.check(targetSource);
|
||||
|
||||
switch (hasPermissionToStorage) {
|
||||
case PermissionTypes.UNDETERMINED:
|
||||
permissionRequest = await Permissions.request(targetSource);
|
||||
if (permissionRequest !== PermissionTypes.AUTHORIZED) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case PermissionTypes.DENIED: {
|
||||
const canOpenSettings = await Permissions.canOpenSettings();
|
||||
let grantOption = null;
|
||||
if (canOpenSettings) {
|
||||
grantOption = {
|
||||
switch (hasPermission) {
|
||||
case Permissions.RESULTS.DENIED:
|
||||
case Permissions.RESULTS.UNAVAILABLE: {
|
||||
const permissionRequest = await Permissions.request(targetSource);
|
||||
|
||||
return permissionRequest === Permissions.RESULTS.GRANTED;
|
||||
}
|
||||
case Permissions.RESULTS.BLOCKED: {
|
||||
const grantOption = {
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
onPress: () => Permissions.openSettings(),
|
||||
};
|
||||
|
||||
const {title, text} = this.getPermissionDeniedMessage();
|
||||
|
||||
Alert.alert(
|
||||
title,
|
||||
text,
|
||||
[
|
||||
grantOption,
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_retry',
|
||||
defaultMessage: 'Settings',
|
||||
id: 'mobile.permission_denied_dismiss',
|
||||
defaultMessage: 'Don\'t Allow',
|
||||
}),
|
||||
onPress: () => Permissions.openSettings(),
|
||||
};
|
||||
}
|
||||
|
||||
const {title, text} = this.getPermissionDeniedMessage();
|
||||
|
||||
Alert.alert(
|
||||
title,
|
||||
text,
|
||||
[
|
||||
grantOption,
|
||||
{
|
||||
text: formatMessage({
|
||||
id: 'mobile.permission_denied_dismiss',
|
||||
defaultMessage: 'Don\'t Allow',
|
||||
}),
|
||||
},
|
||||
],
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
uploadFiles = async (files) => {
|
||||
const file = files[0];
|
||||
if (!file.fileSize | !file.fileName) {
|
||||
const path = (file.path || file.uri).replace('file://', '');
|
||||
const fileInfo = await RNFetchBlob.fs.stat(path);
|
||||
file.fileSize = fileInfo.size;
|
||||
file.fileName = fileInfo.filename;
|
||||
}
|
||||
|
||||
if (!file.type) {
|
||||
file.type = lookupMimeType(file.fileName);
|
||||
}
|
||||
|
||||
const {validMimeTypes} = this.props;
|
||||
if (validMimeTypes.length && !validMimeTypes.includes(file.type)) {
|
||||
this.props.onShowUnsupportedMimeTypeWarning();
|
||||
} else if (file.fileSize > this.props.maxFileSize) {
|
||||
this.props.onShowFileSizeWarning(file.fileName);
|
||||
} else {
|
||||
this.props.uploadFiles(files);
|
||||
}
|
||||
};
|
||||
|
||||
handleButtonPress = () => {
|
||||
const {
|
||||
fileCount,
|
||||
@@ -199,26 +166,19 @@ export default class ImageUploadButton extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
const {theme, buttonContainerStyle} = this.props;
|
||||
return (
|
||||
<TouchableWithFeedback
|
||||
onPress={this.handleButtonPress}
|
||||
style={style.buttonContainer}
|
||||
style={buttonContainerStyle}
|
||||
type={'opacity'}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
color={theme.centerChannelColor}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
name='image-outline'
|
||||
size={20}
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
buttonContainer: {
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Alert, Platform} from 'react-native';
|
||||
import {shallow} from 'enzyme';
|
||||
import Permissions from 'react-native-permissions';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
import ImageUploadButton from './image_upload_button';
|
||||
|
||||
jest.mock('react-intl');
|
||||
jest.mock('react-native-image-picker', () => ({
|
||||
launchCamera: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('ImageUploadButton', () => {
|
||||
const formatMessage = jest.fn();
|
||||
const baseProps = {
|
||||
blurTextBox: jest.fn(),
|
||||
fileCount: 0,
|
||||
maxFileCount: 5,
|
||||
onShowFileMaxWarning: jest.fn(),
|
||||
theme: Preferences.THEMES.default,
|
||||
uploadFiles: jest.fn(),
|
||||
buttonContainerStyle: {},
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<ImageUploadButton {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should return permission false if permission is denied in iOS', async () => {
|
||||
jest.spyOn(Permissions, 'check').mockReturnValue(Permissions.RESULTS.UNAVAILABLE);
|
||||
jest.spyOn(Permissions, 'request').mockReturnValue(Permissions.RESULTS.DENIED);
|
||||
|
||||
const wrapper = shallow(
|
||||
<ImageUploadButton {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
const hasPermission = await wrapper.instance().hasPhotoPermission();
|
||||
expect(Permissions.check).toHaveBeenCalled();
|
||||
expect(Permissions.request).toHaveBeenCalled();
|
||||
expect(Alert.alert).not.toHaveBeenCalled();
|
||||
expect(hasPermission).toBe(false);
|
||||
});
|
||||
|
||||
test('should show permission denied alert and return permission false if permission is blocked in iOS', async () => {
|
||||
jest.spyOn(Permissions, 'check').mockReturnValue(Permissions.RESULTS.BLOCKED);
|
||||
jest.spyOn(Alert, 'alert').mockReturnValue(true);
|
||||
|
||||
const wrapper = shallow(
|
||||
<ImageUploadButton {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
|
||||
const hasPermission = await wrapper.instance().hasPhotoPermission();
|
||||
expect(Permissions.check).toHaveBeenCalled();
|
||||
expect(Permissions.request).not.toHaveBeenCalled();
|
||||
expect(Alert.alert).toHaveBeenCalled();
|
||||
expect(hasPermission).toBe(false);
|
||||
});
|
||||
|
||||
test('hasPhotoPermission returns true when permission has been granted', async () => {
|
||||
const platformPermissions = [{
|
||||
platform: 'ios',
|
||||
permission: Permissions.PERMISSIONS.IOS.PHOTO_LIBRARY,
|
||||
}, {
|
||||
platform: 'android',
|
||||
permission: Permissions.PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE,
|
||||
}];
|
||||
|
||||
for (let i = 0; i < platformPermissions.length; i++) {
|
||||
const {platform, permission} = platformPermissions[i];
|
||||
Platform.OS = platform;
|
||||
|
||||
const check = jest.spyOn(Permissions, 'check');
|
||||
const request = jest.spyOn(Permissions, 'request');
|
||||
request.mockReturnValue(Permissions.RESULTS.GRANTED);
|
||||
|
||||
const wrapper = shallow(
|
||||
<ImageUploadButton {...baseProps}/>,
|
||||
{context: {intl: {formatMessage}}},
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
|
||||
check.mockReturnValueOnce(Permissions.RESULTS.DENIED);
|
||||
let hasPermission = await instance.hasPhotoPermission(); // eslint-disable-line no-await-in-loop
|
||||
expect(check).toHaveBeenCalledWith(permission);
|
||||
expect(request).toHaveBeenCalled();
|
||||
expect(hasPermission).toBe(true);
|
||||
|
||||
check.mockReturnValueOnce(Permissions.RESULTS.UNAVAILABLE);
|
||||
hasPermission = await instance.hasPhotoPermission(); // eslint-disable-line no-await-in-loop
|
||||
expect(check).toHaveBeenCalledWith(permission);
|
||||
expect(request).toHaveBeenCalled();
|
||||
expect(hasPermission).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -14,9 +14,9 @@ import PasteableTextInput from 'app/components/pasteable_text_input';
|
||||
import EphemeralStore from 'app/store/ephemeral_store';
|
||||
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import FileUploadButton from './components/fileUploadButton';
|
||||
import ImageUploadButton from './components/imageUploadButton';
|
||||
import CameraButton from './components/cameraButton';
|
||||
import FileUploadButton from './components/file_upload_button';
|
||||
import ImageUploadButton from './components/image_upload_button';
|
||||
import CameraButton from './components/camera_button';
|
||||
|
||||
import PostTextbox from './post_textbox.ios';
|
||||
|
||||
@@ -344,7 +344,7 @@ describe('PostTextBox', () => {
|
||||
mockResolvedValue({data: 'success'});
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<PostTextbox {...props}/>
|
||||
<PostTextbox {...props}/>,
|
||||
);
|
||||
|
||||
const msg = '/fail preserve this text in the post draft';
|
||||
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
AppState,
|
||||
BackHandler,
|
||||
findNodeHandle,
|
||||
Image,
|
||||
InteractionManager,
|
||||
Keyboard,
|
||||
NativeModules,
|
||||
Platform,
|
||||
@@ -17,8 +15,10 @@ import {
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
View,
|
||||
Image,
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import RNFetchBlob from 'rn-fetch-blob';
|
||||
import Button from 'react-native-button';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import slashForwardBoxIcon from 'assets/images/icons/slash-forward-box.png';
|
||||
@@ -27,15 +27,15 @@ import {General, RequestStatus} from 'mattermost-redux/constants';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {getFormattedFileSize} from 'mattermost-redux/utils/file_utils';
|
||||
|
||||
import FileUploadButton from './components/fileUploadButton';
|
||||
import ImageUploadButton from './components/imageUploadButton';
|
||||
import CameraButton from './components/cameraButton';
|
||||
import FileUploadButton from './components/file_upload_button';
|
||||
import ImageUploadButton from './components/image_upload_button';
|
||||
import CameraButton from './components/camera_button';
|
||||
import FormattedMarkdownText from 'app/components/formatted_markdown_text';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import PasteableTextInput from 'app/components/pasteable_text_input';
|
||||
import {paddingHorizontal as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
import SendButton from 'app/components/send_button';
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT, IS_REACTION_REGEX, MAX_FILE_COUNT} from 'app/constants/post_textbox';
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT, IS_REACTION_REGEX, MAX_FILE_COUNT, ICON_SIZE} from 'app/constants/post_textbox';
|
||||
import {NOTIFY_ALL_MEMBERS} from 'app/constants/view';
|
||||
import FileUploadPreview from 'app/components/file_upload_preview';
|
||||
|
||||
@@ -49,6 +49,8 @@ import {
|
||||
} from 'app/utils/theme';
|
||||
|
||||
const {RNTextInputReset} = NativeModules;
|
||||
const INPUT_LINE_HEIGHT = 20;
|
||||
const EXTRA_INPUT_PADDING = 3;
|
||||
|
||||
export default class PostTextBoxBase extends PureComponent {
|
||||
static propTypes = {
|
||||
@@ -120,6 +122,7 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
channelId: props.channelId,
|
||||
channelTimezoneCount: 0,
|
||||
longMessageAlertShown: false,
|
||||
extraInputPadding: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -239,13 +242,23 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
startAtMention = () => {
|
||||
this.handleTextChange(`${this.state.value}@`, true);
|
||||
this.focus();
|
||||
};
|
||||
|
||||
startSlashCommand = () => {
|
||||
this.handleTextChange('/', true);
|
||||
this.focus();
|
||||
};
|
||||
|
||||
getTextInputButton = (actionType) => {
|
||||
const {channelIsReadOnly, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
let button = null;
|
||||
const buttonStyle = [];
|
||||
let iconColor = theme.centerChannelColor;
|
||||
let iconColor = changeOpacity(theme.centerChannelColor, 0.64);
|
||||
let isDisabled = false;
|
||||
|
||||
if (!channelIsReadOnly) {
|
||||
@@ -253,21 +266,18 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
case 'at':
|
||||
isDisabled = this.state.value[this.state.value.length - 1] === '@';
|
||||
if (isDisabled) {
|
||||
iconColor = changeOpacity(theme.centerChannelColor, 0.6);
|
||||
iconColor = changeOpacity(theme.centerChannelColor, 0.16);
|
||||
}
|
||||
button = (
|
||||
<TouchableOpacity
|
||||
disabled={isDisabled}
|
||||
onPress={() => {
|
||||
this.handleTextChange(`${this.state.value}@`, true);
|
||||
this.focus();
|
||||
}}
|
||||
onPress={this.startAtMention}
|
||||
style={style.iconWrapper}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
color={iconColor}
|
||||
name='at'
|
||||
size={20}
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -282,10 +292,7 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
button = (
|
||||
<TouchableOpacity
|
||||
disabled={isDisabled}
|
||||
onPress={() => {
|
||||
this.handleTextChange('/', true);
|
||||
this.focus();
|
||||
}}
|
||||
onPress={this.startSlashCommand}
|
||||
style={style.iconWrapper}
|
||||
>
|
||||
<Image
|
||||
@@ -303,6 +310,7 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
|
||||
getMediaButton = (actionType) => {
|
||||
const {canUploadFiles, channelIsReadOnly, files, maxFileSize, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
let button = null;
|
||||
const props = {
|
||||
blurTextBox: this.blur,
|
||||
@@ -313,6 +321,7 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
uploadFiles: this.handleUploadFiles,
|
||||
maxFileSize,
|
||||
theme,
|
||||
buttonContainerStyle: style.iconWrapper,
|
||||
};
|
||||
|
||||
if (canUploadFiles && !channelIsReadOnly) {
|
||||
@@ -504,8 +513,20 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
handleUploadFiles = (files) => {
|
||||
this.props.actions.initUploadFiles(files, this.props.rootId);
|
||||
handleUploadFiles = async (files) => {
|
||||
const file = files[0];
|
||||
if (!file.fileSize | !file.fileName) {
|
||||
const path = (file.path || file.uri).replace('file://', '');
|
||||
const fileInfo = await RNFetchBlob.fs.stat(path);
|
||||
file.fileSize = fileInfo.size;
|
||||
file.fileName = fileInfo.filename;
|
||||
}
|
||||
|
||||
if (file.fileSize > this.props.maxFileSize) {
|
||||
this.onShowFileSizeWarning(file.fileName);
|
||||
} else {
|
||||
this.props.actions.initUploadFiles(files, this.props.rootId);
|
||||
}
|
||||
};
|
||||
|
||||
isFileLoading = () => {
|
||||
@@ -621,10 +642,26 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
actions.handleClearFiles(channelId, rootId);
|
||||
}
|
||||
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
if (Platform.OS === 'ios') {
|
||||
// On iOS, if the PostTextbox height increases from its
|
||||
// initial height (due to a multiline post or a post whose
|
||||
// message wraps, for example), then when the text is cleared
|
||||
// the PostTextbox height decrease will be animated. This
|
||||
// animation in conjunction with the PostList animation as it
|
||||
// receives the newly created post is causing issues in the iOS
|
||||
// PostList component as it fails to properly react to its content
|
||||
// size changes. While a proper fix is determined for the PostList
|
||||
// component, a small delay in triggering the height decrease
|
||||
// animation gives the PostList enough time to first handle content
|
||||
// size changes from the new post.
|
||||
setTimeout(() => {
|
||||
this.handleTextChange('');
|
||||
this.setState({sendingMessage: false});
|
||||
}, 250);
|
||||
} else {
|
||||
this.handleTextChange('');
|
||||
this.setState({sendingMessage: false});
|
||||
});
|
||||
}
|
||||
|
||||
this.changeDraft('');
|
||||
|
||||
@@ -722,7 +759,7 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
EventEmitter.emit('fileSizeWarning', fileSizeWarning);
|
||||
setTimeout(() => {
|
||||
EventEmitter.emit('fileSizeWarning', null);
|
||||
}, 3000);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
onCloseChannelPress = () => {
|
||||
@@ -808,6 +845,16 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
handleInputSizeChange = ({nativeEvent: {contentSize}}) => {
|
||||
const {extraInputPadding} = this.state;
|
||||
const numLines = contentSize.height / INPUT_LINE_HEIGHT;
|
||||
if (numLines >= 2 && extraInputPadding !== EXTRA_INPUT_PADDING) {
|
||||
this.setState({extraInputPadding: EXTRA_INPUT_PADDING});
|
||||
} else if (numLines < 2 && extraInputPadding !== 0) {
|
||||
this.setState({extraInputPadding: 0});
|
||||
}
|
||||
}
|
||||
|
||||
renderDeactivatedChannel = () => {
|
||||
const {intl} = this.context;
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
@@ -831,10 +878,22 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
return this.archivedView(theme, style);
|
||||
}
|
||||
|
||||
const {value} = this.state;
|
||||
const {value, extraInputPadding} = this.state;
|
||||
const textValue = channelIsLoading ? '' : value;
|
||||
const placeholder = this.getPlaceHolder();
|
||||
|
||||
let maxHeight = 150;
|
||||
|
||||
if (isLandscape) {
|
||||
maxHeight = 88;
|
||||
}
|
||||
|
||||
const inputStyle = {};
|
||||
if (extraInputPadding) {
|
||||
inputStyle.paddingBottom = style.input.paddingBottom + extraInputPadding;
|
||||
inputStyle.paddingTop = style.input.paddingTop + extraInputPadding;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[style.inputWrapper, padding(isLandscape)]}
|
||||
@@ -854,6 +913,7 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
<PasteableTextInput
|
||||
ref={this.input}
|
||||
value={textValue}
|
||||
style={{...style.input, ...inputStyle, maxHeight}}
|
||||
onChangeText={this.handleTextChange}
|
||||
onSelectionChange={this.handlePostDraftSelectionChanged}
|
||||
placeholder={intl.formatMessage(placeholder, {channelDisplayName})}
|
||||
@@ -861,40 +921,43 @@ export default class PostTextBoxBase extends PureComponent {
|
||||
multiline={true}
|
||||
blurOnSubmit={false}
|
||||
underlineColorAndroid='transparent'
|
||||
style={style.input}
|
||||
keyboardType={this.state.keyboardType}
|
||||
onEndEditing={this.handleEndEditing}
|
||||
disableFullscreenUI={true}
|
||||
editable={!channelIsReadOnly}
|
||||
onPaste={this.handlePasteFiles}
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
onContentSizeChange={Platform.OS === 'android' ? this.handleInputSizeChange : null}
|
||||
/>
|
||||
|
||||
<FileUploadPreview
|
||||
files={files}
|
||||
rootId={rootId}
|
||||
/>
|
||||
|
||||
<View style={style.buttonsContainer}>
|
||||
<View style={style.quickActionsContainer}>
|
||||
|
||||
{this.getTextInputButton('at')}
|
||||
|
||||
{this.getTextInputButton('slash')}
|
||||
|
||||
{this.getMediaButton('file')}
|
||||
|
||||
{this.getMediaButton('image')}
|
||||
|
||||
{this.getMediaButton('camera')}
|
||||
|
||||
</View>
|
||||
<SendButton
|
||||
disabled={!this.isSendButtonEnabled()}
|
||||
handleSendMessage={this.handleSendMessage}
|
||||
theme={theme}
|
||||
{!channelIsReadOnly &&
|
||||
<React.Fragment>
|
||||
<FileUploadPreview
|
||||
files={files}
|
||||
rootId={rootId}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={style.buttonsContainer}>
|
||||
<View style={style.quickActionsContainer}>
|
||||
|
||||
{this.getTextInputButton('at')}
|
||||
|
||||
{this.getTextInputButton('slash')}
|
||||
|
||||
{this.getMediaButton('file')}
|
||||
|
||||
{this.getMediaButton('image')}
|
||||
|
||||
{this.getMediaButton('camera')}
|
||||
|
||||
</View>
|
||||
<SendButton
|
||||
disabled={!this.isSendButtonEnabled()}
|
||||
handleSendMessage={this.handleSendMessage}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
@@ -908,48 +971,61 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingBottom: Platform.select({
|
||||
ios: 1,
|
||||
android: 2,
|
||||
}),
|
||||
},
|
||||
slashIcon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
width: ICON_SIZE,
|
||||
height: ICON_SIZE,
|
||||
opacity: 1,
|
||||
tintColor: theme.centerChannelColor,
|
||||
tintColor: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
},
|
||||
iconDisabled: {
|
||||
tintColor: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
tintColor: changeOpacity(theme.centerChannelColor, 0.16),
|
||||
},
|
||||
iconWrapper: {
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 10,
|
||||
},
|
||||
quickActionsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
height: 44,
|
||||
},
|
||||
input: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 14,
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
paddingTop: 8,
|
||||
maxHeight: 150,
|
||||
fontSize: 15,
|
||||
lineHeight: INPUT_LINE_HEIGHT,
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: Platform.select({
|
||||
ios: 6,
|
||||
android: 8,
|
||||
}),
|
||||
paddingBottom: Platform.select({
|
||||
ios: 6,
|
||||
android: 2,
|
||||
}),
|
||||
minHeight: 30,
|
||||
},
|
||||
inputContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
marginRight: 10,
|
||||
marginLeft: 10,
|
||||
},
|
||||
inputContentContainer: {
|
||||
alignItems: 'stretch',
|
||||
paddingTop: Platform.select({
|
||||
ios: 7,
|
||||
android: 0,
|
||||
}),
|
||||
},
|
||||
inputWrapper: {
|
||||
alignItems: 'flex-end',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 4,
|
||||
paddingBottom: 2,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.20),
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('ProgressiveImage', () => {
|
||||
<ProgressiveImage
|
||||
{...baseProps}
|
||||
thumbnailUri={null}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
jest.spyOn(instance, 'setImage');
|
||||
@@ -73,7 +73,7 @@ describe('ProgressiveImage', () => {
|
||||
<ProgressiveImage
|
||||
{...baseProps}
|
||||
imageUri={null}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
jest.spyOn(instance, 'setThumbnail');
|
||||
|
||||
@@ -44,14 +44,14 @@ class RadioButton extends PureComponent {
|
||||
{
|
||||
toValue: 1,
|
||||
duration: 150,
|
||||
}
|
||||
},
|
||||
).start();
|
||||
Animated.timing(
|
||||
this.state.opacityValue,
|
||||
{
|
||||
toValue: 0.1,
|
||||
duration: 100,
|
||||
}
|
||||
},
|
||||
).start();
|
||||
};
|
||||
|
||||
@@ -61,13 +61,13 @@ class RadioButton extends PureComponent {
|
||||
{
|
||||
toValue: 0.001,
|
||||
duration: 1500,
|
||||
}
|
||||
},
|
||||
).start();
|
||||
Animated.timing(
|
||||
this.state.opacityValue,
|
||||
{
|
||||
toValue: 0,
|
||||
}
|
||||
},
|
||||
).start();
|
||||
};
|
||||
|
||||
|
||||
@@ -162,13 +162,13 @@ export default class Reactions extends PureComponent {
|
||||
case 'right':
|
||||
reactionElements.push(
|
||||
this.renderReactions(),
|
||||
addMoreReactions
|
||||
addMoreReactions,
|
||||
);
|
||||
break;
|
||||
case 'left':
|
||||
reactionElements.push(
|
||||
addMoreReactions,
|
||||
this.renderReactions()
|
||||
this.renderReactions(),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ export default class Root extends PureComponent {
|
||||
|
||||
return (
|
||||
<IntlProvider
|
||||
key={locale}
|
||||
ref={this.setProviderRef}
|
||||
locale={locale}
|
||||
messages={getTranslations(locale)}
|
||||
|
||||
@@ -111,7 +111,7 @@ export default class SafeAreaIos extends PureComponent {
|
||||
if (this.mounted) {
|
||||
this.setState({statusBarHeight: statusBarFrameData.height});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// not needed
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('SafeAreaIos', () => {
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
@@ -91,7 +91,7 @@ describe('SafeAreaIos', () => {
|
||||
mattermostManaged.hasSafeAreaInsets = false;
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(SafeArea.getSafeAreaInsetsForRootView).toHaveBeenCalled();
|
||||
@@ -104,7 +104,7 @@ describe('SafeAreaIos', () => {
|
||||
mattermostManaged.hasSafeAreaInsets = true;
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(SafeArea.getSafeAreaInsetsForRootView).toHaveBeenCalled();
|
||||
@@ -117,7 +117,7 @@ describe('SafeAreaIos', () => {
|
||||
mattermostManaged.hasSafeAreaInsets = false;
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(SafeArea.getSafeAreaInsetsForRootView).not.toHaveBeenCalled();
|
||||
@@ -130,7 +130,7 @@ describe('SafeAreaIos', () => {
|
||||
mattermostManaged.hasSafeAreaInsets = false;
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
|
||||
@@ -145,7 +145,7 @@ describe('SafeAreaIos', () => {
|
||||
mattermostManaged.hasSafeAreaInsets = true;
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
|
||||
@@ -160,7 +160,7 @@ describe('SafeAreaIos', () => {
|
||||
mattermostManaged.hasSafeAreaInsets = false;
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
|
||||
@@ -175,7 +175,7 @@ describe('SafeAreaIos', () => {
|
||||
mattermostManaged.hasSafeAreaInsets = true;
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(TEST_INSETS_2.safeAreaInsets);
|
||||
@@ -188,7 +188,7 @@ describe('SafeAreaIos', () => {
|
||||
|
||||
test('should set portrait safe area insets', () => {
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(PORTRAIT_INSETS.safeAreaInsets);
|
||||
@@ -205,7 +205,7 @@ describe('SafeAreaIos', () => {
|
||||
|
||||
test('should set portrait safe area insets from EphemeralStore', () => {
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
EphemeralStore.safeAreaInsets[PORTRAIT] = PORTRAIT_INSETS.safeAreaInsets;
|
||||
@@ -221,7 +221,7 @@ describe('SafeAreaIos', () => {
|
||||
|
||||
test('should set landscape safe area insets', () => {
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.state().safeAreaInsets).not.toEqual(LANDSCAPE_INSETS.safeAreaInsets);
|
||||
@@ -238,7 +238,7 @@ describe('SafeAreaIos', () => {
|
||||
|
||||
test('should set landscape safe area insets from EphemeralStore', () => {
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
|
||||
EphemeralStore.safeAreaInsets[LANDSCAPE] = LANDSCAPE_INSETS.safeAreaInsets;
|
||||
@@ -258,7 +258,7 @@ describe('SafeAreaIos', () => {
|
||||
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
|
||||
expect(EphemeralStore.safeAreaInsets[LANDSCAPE]).toEqual(null);
|
||||
let wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
let instance = wrapper.instance();
|
||||
expect(addEventListener).toHaveBeenCalledWith('safeAreaInsetsForRootViewDidChange', instance.onSafeAreaInsetsForRootViewChange);
|
||||
@@ -266,7 +266,7 @@ describe('SafeAreaIos', () => {
|
||||
|
||||
EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets;
|
||||
wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
instance = wrapper.instance();
|
||||
expect(addEventListener).toHaveBeenCalledWith('safeAreaInsetsForRootViewDidChange', instance.onSafeAreaInsetsForRootViewChange);
|
||||
@@ -275,7 +275,7 @@ describe('SafeAreaIos', () => {
|
||||
EphemeralStore.safeAreaInsets[PORTRAIT] = TEST_INSETS_1.safeAreaInsets;
|
||||
EphemeralStore.safeAreaInsets[LANDSCAPE] = TEST_INSETS_1.safeAreaInsets;
|
||||
wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
instance = wrapper.instance();
|
||||
expect(addEventListener).not.toHaveBeenCalled();
|
||||
@@ -285,7 +285,7 @@ describe('SafeAreaIos', () => {
|
||||
const removeEventListener = jest.spyOn(SafeArea, 'removeEventListener');
|
||||
|
||||
const wrapper = shallow(
|
||||
<SafeAreaIos {...baseProps}/>
|
||||
<SafeAreaIos {...baseProps}/>,
|
||||
);
|
||||
const instance = wrapper.instance();
|
||||
expect(EphemeralStore.safeAreaInsets[PORTRAIT]).toEqual(null);
|
||||
|
||||
@@ -202,7 +202,7 @@ export default class Search extends Component {
|
||||
toValue: (text.length > 0) ? 1 : 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}
|
||||
},
|
||||
).start();
|
||||
|
||||
if (this.props.onChangeText) {
|
||||
@@ -228,7 +228,7 @@ export default class Search extends Component {
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}
|
||||
},
|
||||
).start();
|
||||
this.focus();
|
||||
|
||||
@@ -258,42 +258,42 @@ export default class Search extends Component {
|
||||
{
|
||||
toValue: this.contentWidth - 90,
|
||||
duration: 200,
|
||||
}
|
||||
},
|
||||
),
|
||||
Animated.timing(
|
||||
this.inputFocusAnimated,
|
||||
{
|
||||
toValue: this.state.leftComponentWidth,
|
||||
duration: 200,
|
||||
}
|
||||
},
|
||||
),
|
||||
Animated.timing(
|
||||
this.leftComponentAnimated,
|
||||
{
|
||||
toValue: this.contentWidth,
|
||||
duration: 200,
|
||||
}
|
||||
},
|
||||
),
|
||||
Animated.timing(
|
||||
this.btnCancelAnimated,
|
||||
{
|
||||
toValue: this.state.leftComponentWidth ? 15 - this.state.leftComponentWidth : 5,
|
||||
duration: 200,
|
||||
}
|
||||
},
|
||||
),
|
||||
Animated.timing(
|
||||
this.inputFocusPlaceholderAnimated,
|
||||
{
|
||||
toValue: this.props.placeholderExpandedMargin,
|
||||
duration: 200,
|
||||
}
|
||||
},
|
||||
),
|
||||
Animated.timing(
|
||||
this.iconSearchAnimated,
|
||||
{
|
||||
toValue: this.props.searchIconExpandedMargin,
|
||||
duration: 200,
|
||||
}
|
||||
},
|
||||
),
|
||||
Animated.timing(
|
||||
this.iconDeleteAnimated,
|
||||
@@ -301,7 +301,7 @@ export default class Search extends Component {
|
||||
toValue: (this.props.value.length > 0) ? 1 : 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}
|
||||
},
|
||||
),
|
||||
Animated.timing(
|
||||
this.shadowOpacityAnimated,
|
||||
@@ -309,7 +309,7 @@ export default class Search extends Component {
|
||||
toValue: this.props.shadowOpacityExpanded,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}
|
||||
},
|
||||
),
|
||||
]).start();
|
||||
this.shadowHeight = this.props.shadowOffsetHeightExpanded;
|
||||
@@ -326,28 +326,28 @@ export default class Search extends Component {
|
||||
{
|
||||
toValue: this.contentWidth - this.state.leftComponentWidth - this.props.inputCollapsedMargin,
|
||||
duration: 200,
|
||||
}
|
||||
},
|
||||
),
|
||||
Animated.timing(
|
||||
this.inputFocusAnimated,
|
||||
{
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
}
|
||||
},
|
||||
),
|
||||
Animated.timing(
|
||||
this.leftComponentAnimated,
|
||||
{
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
}
|
||||
},
|
||||
),
|
||||
Animated.timing(
|
||||
this.btnCancelAnimated,
|
||||
{
|
||||
toValue: this.contentWidth,
|
||||
duration: 200,
|
||||
}
|
||||
},
|
||||
),
|
||||
((this.props.keyboardShouldPersist === false) ?
|
||||
Animated.timing(
|
||||
@@ -355,7 +355,7 @@ export default class Search extends Component {
|
||||
{
|
||||
toValue: this.props.placeholderCollapsedMargin,
|
||||
duration: 200,
|
||||
}
|
||||
},
|
||||
) : null),
|
||||
((this.props.keyboardShouldPersist === false || isForceAnim === true) ?
|
||||
Animated.timing(
|
||||
@@ -363,7 +363,7 @@ export default class Search extends Component {
|
||||
{
|
||||
toValue: (this.props.searchIconCollapsedMargin + this.state.leftComponentWidth),
|
||||
duration: 200,
|
||||
}
|
||||
},
|
||||
) : null),
|
||||
Animated.timing(
|
||||
this.iconDeleteAnimated,
|
||||
@@ -371,7 +371,7 @@ export default class Search extends Component {
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}
|
||||
},
|
||||
),
|
||||
Animated.timing(
|
||||
this.shadowOpacityAnimated,
|
||||
@@ -379,7 +379,7 @@ export default class Search extends Component {
|
||||
toValue: this.props.shadowOpacityCollapsed,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}
|
||||
},
|
||||
),
|
||||
]).start(({finished}) => this.props.onAnimationComplete(finished));
|
||||
this.shadowHeight = this.props.shadowOffsetHeightCollapsed;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {memo} from 'react';
|
||||
import {Platform, View} from 'react-native';
|
||||
import {View} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import TouchableWithFeedback from 'app/components/touchable_with_feedback';
|
||||
@@ -18,19 +18,15 @@ function SendButton(props) {
|
||||
PaperPlane = require('app/components/paper_plane').default;
|
||||
}
|
||||
|
||||
const icon = (
|
||||
<PaperPlane
|
||||
height={13}
|
||||
width={15}
|
||||
color={theme.buttonColor}
|
||||
/>
|
||||
);
|
||||
|
||||
if (props.disabled) {
|
||||
return (
|
||||
<View style={style.sendButtonContainer}>
|
||||
<View style={[style.sendButton, style.disableButton]}>
|
||||
{icon}
|
||||
<PaperPlane
|
||||
height={16}
|
||||
width={19}
|
||||
color={changeOpacity(theme.buttonColor, 0.5)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -43,7 +39,11 @@ function SendButton(props) {
|
||||
type={'opacity'}
|
||||
>
|
||||
<View style={style.sendButton}>
|
||||
{icon}
|
||||
<PaperPlane
|
||||
height={16}
|
||||
width={19}
|
||||
color={theme.buttonColor}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
);
|
||||
@@ -62,20 +62,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
},
|
||||
sendButtonContainer: {
|
||||
justifyContent: 'flex-end',
|
||||
paddingHorizontal: 5,
|
||||
paddingVertical: Platform.select({
|
||||
android: 8,
|
||||
ios: 2,
|
||||
}),
|
||||
paddingRight: 8,
|
||||
},
|
||||
sendButton: {
|
||||
backgroundColor: theme.buttonBg,
|
||||
borderRadius: 4,
|
||||
height: 28,
|
||||
width: 72,
|
||||
height: 32,
|
||||
width: 80,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingLeft: 3,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('SendButton', () => {
|
||||
<SendButton
|
||||
{...baseProps}
|
||||
{...props}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('ShowMoreButton', () => {
|
||||
|
||||
test('should match, full snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<ShowMoreButton {...baseProps}/>
|
||||
<ShowMoreButton {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
@@ -28,7 +28,7 @@ describe('ShowMoreButton', () => {
|
||||
|
||||
test('should match, button snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<ShowMoreButton {...baseProps}/>
|
||||
<ShowMoreButton {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.instance().renderButton(true, {button: {}, sign: {}, text: {}})).toMatchSnapshot();
|
||||
@@ -37,7 +37,7 @@ describe('ShowMoreButton', () => {
|
||||
|
||||
test('should LinearGradient exists', () => {
|
||||
const wrapper = shallow(
|
||||
<ShowMoreButton {...baseProps}/>
|
||||
<ShowMoreButton {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find(LinearGradient).exists()).toBe(true);
|
||||
@@ -51,7 +51,7 @@ describe('ShowMoreButton', () => {
|
||||
<ShowMoreButton
|
||||
{...baseProps}
|
||||
onPress={onPress}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
|
||||
wrapper.find(TouchableWithFeedback).props().onPress();
|
||||
|
||||
@@ -417,7 +417,11 @@ export default class DrawerLayout extends Component {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (moveX <= 35 && dx > 0) {
|
||||
const filter = Platform.select({
|
||||
ios: moveX > 0 && dx > 35,
|
||||
android: moveX <= 35 && dx > 0
|
||||
});
|
||||
if (filter) {
|
||||
this._isClosing = false;
|
||||
return true;
|
||||
}
|
||||
@@ -449,8 +453,9 @@ export default class DrawerLayout extends Component {
|
||||
this._emitStateChanged(DRAGGING);
|
||||
};
|
||||
|
||||
_panResponderMove = (e: EventType, { moveX }: PanResponderEventType) => {
|
||||
let openValue = this._getOpenValueForX(moveX);
|
||||
_panResponderMove = (e: EventType, { moveX, dx }: PanResponderEventType) => {
|
||||
const useDx = Platform.OS === 'ios' && this.getDrawerPosition() === 'left' && !this._isClosing;
|
||||
let openValue = this._getOpenValueForX(useDx ? dx : moveX);
|
||||
|
||||
if (this._isClosing) {
|
||||
openValue = 1 - (this._closingAnchorValue - openValue);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,11 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Animated,
|
||||
TouchableHighlight,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {paddingLeft as padding} from 'app/components/safe_area_view/iphone_x_spacing';
|
||||
@@ -19,8 +17,6 @@ import ChannelIcon from 'app/components/channel_icon';
|
||||
import {preventDoubleTap} from 'app/utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const {View: AnimatedView} = Animated;
|
||||
|
||||
export default class ChannelItem extends PureComponent {
|
||||
static propTypes = {
|
||||
channelId: PropTypes.string.isRequired,
|
||||
@@ -40,7 +36,6 @@ export default class ChannelItem extends PureComponent {
|
||||
unreadMsgs: PropTypes.number.isRequired,
|
||||
isSearchResult: PropTypes.bool,
|
||||
isBot: PropTypes.bool.isRequired,
|
||||
previewChannel: PropTypes.func,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
@@ -61,27 +56,6 @@ export default class ChannelItem extends PureComponent {
|
||||
});
|
||||
});
|
||||
|
||||
onPreview = ({reactTag}) => {
|
||||
const {channelId, previewChannel} = this.props;
|
||||
if (previewChannel) {
|
||||
const {intl} = this.context;
|
||||
const passProps = {
|
||||
channelId,
|
||||
};
|
||||
const options = {
|
||||
preview: {
|
||||
reactTag,
|
||||
actions: [{
|
||||
id: 'action-mark-as-read',
|
||||
title: intl.formatMessage({id: 'mobile.channel.markAsRead', defaultMessage: 'Mark As Read'}),
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
previewChannel(passProps, options);
|
||||
}
|
||||
};
|
||||
|
||||
showChannelAsUnread = () => {
|
||||
return this.props.mentions > 0 || (this.props.unreadMsgs > 0 && this.props.showUnreadForMsgs);
|
||||
};
|
||||
@@ -192,29 +166,25 @@ export default class ChannelItem extends PureComponent {
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatedView>
|
||||
<Navigation.TouchablePreview
|
||||
touchableComponent={TouchableHighlight}
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
|
||||
onPress={this.onPress}
|
||||
onPressIn={this.onPreview}
|
||||
>
|
||||
<View style={[style.container, mutedStyle, padding(isLandscape)]}>
|
||||
{extraBorder}
|
||||
<View style={[style.item, extraItemStyle]}>
|
||||
{icon}
|
||||
<Text
|
||||
style={[style.text, extraTextStyle]}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{channelDisplayName}
|
||||
</Text>
|
||||
{badge}
|
||||
</View>
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<View style={[style.container, mutedStyle, padding(isLandscape)]}>
|
||||
{extraBorder}
|
||||
<View style={[style.item, extraItemStyle]}>
|
||||
{icon}
|
||||
<Text
|
||||
style={[style.text, extraTextStyle]}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{channelDisplayName}
|
||||
</Text>
|
||||
{badge}
|
||||
</View>
|
||||
</Navigation.TouchablePreview>
|
||||
</AnimatedView>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import {TouchableHighlight} from 'react-native';
|
||||
|
||||
import Preferences from 'mattermost-redux/constants/preferences';
|
||||
|
||||
@@ -217,7 +217,7 @@ describe('ChannelItem', () => {
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
wrapper.find(Navigation.TouchablePreview).simulate('press');
|
||||
wrapper.find(TouchableHighlight).simulate('press');
|
||||
jest.runAllTimers();
|
||||
|
||||
const expectedChannelParams = {id: baseProps.channelId, display_name: baseProps.displayName, fake: channel.fake, type: channel.type};
|
||||
|
||||
@@ -34,7 +34,6 @@ export default class ChannelsList extends PureComponent {
|
||||
onShowTeams: PropTypes.func.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
drawerOpened: PropTypes.bool,
|
||||
previewChannel: PropTypes.func,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
@@ -98,7 +97,6 @@ export default class ChannelsList extends PureComponent {
|
||||
const {
|
||||
onShowTeams,
|
||||
theme,
|
||||
previewChannel,
|
||||
isLandscape,
|
||||
} = this.props;
|
||||
|
||||
@@ -112,7 +110,6 @@ export default class ChannelsList extends PureComponent {
|
||||
onSelectChannel={this.onSelectChannel}
|
||||
styles={styles}
|
||||
term={term}
|
||||
previewChannel={previewChannel}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
@@ -120,7 +117,6 @@ export default class ChannelsList extends PureComponent {
|
||||
<List
|
||||
onSelectChannel={this.onSelectChannel}
|
||||
styles={styles}
|
||||
previewChannel={previewChannel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ class FilteredList extends Component {
|
||||
styles: PropTypes.object.isRequired,
|
||||
term: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
previewChannel: PropTypes.func,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
@@ -350,7 +349,6 @@ class FilteredList extends Component {
|
||||
isUnread={item.isUnread}
|
||||
mentions={0}
|
||||
onSelectChannel={this.onSelectChannel}
|
||||
previewChannel={this.props.previewChannel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ const DEFAULT_SEARCH_ORDER = ['unreads', 'dms', 'channels', 'members', 'nonmembe
|
||||
|
||||
const pastDirectMessages = createSelector(
|
||||
getDirectShowPreferences,
|
||||
(directChannelsFromPreferences) => directChannelsFromPreferences.filter((d) => d.value === 'false').map((d) => d.name)
|
||||
(directChannelsFromPreferences) => directChannelsFromPreferences.filter((d) => d.value === 'false').map((d) => d.name),
|
||||
);
|
||||
|
||||
const getTeamProfiles = createSelector(
|
||||
@@ -40,7 +40,7 @@ const getTeamProfiles = createSelector(
|
||||
|
||||
return memberProfiles;
|
||||
}, {});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Fill an object for each group channel with concatenated strings for username, email, fullname, and nickname
|
||||
@@ -93,7 +93,7 @@ const getGroupChannelMemberDetails = createSelector(
|
||||
getUserIdsInChannels,
|
||||
getUsers,
|
||||
getGroupChannels,
|
||||
getGroupDetails
|
||||
getGroupDetails,
|
||||
);
|
||||
|
||||
function mapStateToProps(state) {
|
||||
|
||||
@@ -48,7 +48,6 @@ export default class List extends PureComponent {
|
||||
styles: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
orderedChannelIds: PropTypes.array.isRequired,
|
||||
previewChannel: PropTypes.func,
|
||||
isLandscape: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
@@ -309,7 +308,7 @@ export default class List extends PureComponent {
|
||||
};
|
||||
|
||||
renderItem = ({item}) => {
|
||||
const {favoriteChannelIds, unreadChannelIds, previewChannel} = this.props;
|
||||
const {favoriteChannelIds, unreadChannelIds} = this.props;
|
||||
|
||||
return (
|
||||
<ChannelItem
|
||||
@@ -317,7 +316,6 @@ export default class List extends PureComponent {
|
||||
isUnread={unreadChannelIds.includes(item)}
|
||||
isFavorite={favoriteChannelIds.includes(item)}
|
||||
onSelectChannel={this.onSelectChannel}
|
||||
previewChannel={previewChannel}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -45,7 +45,6 @@ export default class ChannelSidebar extends Component {
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
teamsCount: PropTypes.number.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
previewChannel: PropTypes.func,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -318,7 +317,6 @@ export default class ChannelSidebar extends Component {
|
||||
const {
|
||||
teamsCount,
|
||||
theme,
|
||||
previewChannel,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -372,9 +370,8 @@ export default class ChannelSidebar extends Component {
|
||||
onSearchEnds={this.onSearchEnds}
|
||||
theme={theme}
|
||||
drawerOpened={this.state.drawerOpened}
|
||||
previewChannel={previewChannel}
|
||||
/>
|
||||
</View>
|
||||
</View>,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('MainSidebar', () => {
|
||||
|
||||
test('should match, full snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<MainSidebar {...baseProps}/>
|
||||
<MainSidebar {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
@@ -42,7 +42,7 @@ describe('MainSidebar', () => {
|
||||
|
||||
test('should not set the permanentSidebar state if not Tablet', () => {
|
||||
const wrapper = shallow(
|
||||
<MainSidebar {...baseProps}/>
|
||||
<MainSidebar {...baseProps}/>,
|
||||
);
|
||||
|
||||
wrapper.instance().handlePermanentSidebar();
|
||||
@@ -51,7 +51,7 @@ describe('MainSidebar', () => {
|
||||
|
||||
test('should set the permanentSidebar state if Tablet', async () => {
|
||||
const wrapper = shallow(
|
||||
<MainSidebar {...baseProps}/>
|
||||
<MainSidebar {...baseProps}/>,
|
||||
);
|
||||
|
||||
DeviceTypes.IS_TABLET = true;
|
||||
@@ -73,7 +73,7 @@ describe('MainSidebar', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<MainSidebar {...props}/>
|
||||
<MainSidebar {...props}/>,
|
||||
);
|
||||
|
||||
const instance = wrapper.instance();
|
||||
@@ -88,7 +88,7 @@ describe('MainSidebar', () => {
|
||||
Platform.OS = 'ios';
|
||||
|
||||
const wrapper = shallow(
|
||||
<MainSidebar {...baseProps}/>
|
||||
<MainSidebar {...baseProps}/>,
|
||||
);
|
||||
const drawer = wrapper.dive().childAt(1);
|
||||
const drawerStyle = drawer.props().style.reduce((acc, obj) => ({...acc, ...obj}));
|
||||
@@ -99,7 +99,7 @@ describe('MainSidebar', () => {
|
||||
Platform.OS = 'android';
|
||||
|
||||
const wrapper = shallow(
|
||||
<MainSidebar {...baseProps}/>
|
||||
<MainSidebar {...baseProps}/>,
|
||||
);
|
||||
const drawer = wrapper.dive().childAt(1);
|
||||
const drawerStyle = drawer.props().style.reduce((acc, obj) => ({...acc, ...obj}));
|
||||
|
||||
@@ -178,7 +178,7 @@ export default class SettingsDrawer extends PureComponent {
|
||||
this.openModal(
|
||||
'EditProfile',
|
||||
formatMessage({id: 'mobile.routes.edit_profile', defaultMessage: 'Edit Profile'}),
|
||||
{currentUser, commandType}
|
||||
{currentUser, commandType},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -207,7 +207,7 @@ export default class SettingsDrawer extends PureComponent {
|
||||
this.openModal(
|
||||
'UserProfile',
|
||||
formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
|
||||
{userId, fromSettings: true}
|
||||
{userId, fromSettings: true},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -111,13 +111,13 @@ export default class SlideUpPanel extends PureComponent {
|
||||
|
||||
this.reverseLastScrollY = Animated.multiply(
|
||||
new Animated.Value(-1),
|
||||
this.lastScrollY
|
||||
this.lastScrollY,
|
||||
);
|
||||
|
||||
this.translateYOffset = new Animated.Value(containerHeight);
|
||||
this.translateY = Animated.add(
|
||||
this.translateYOffset,
|
||||
Animated.add(this.dragY, this.reverseLastScrollY)
|
||||
Animated.add(this.dragY, this.reverseLastScrollY),
|
||||
).interpolate({
|
||||
inputRange: [marginFromTop, containerHeight],
|
||||
outputRange: [marginFromTop, containerHeight],
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('SlideUpPanelIndicator', () => {
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<SlideUpPanelIndicator {...baseProps}/>
|
||||
<SlideUpPanelIndicator {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {View} from 'react-native';
|
||||
import Svg, {
|
||||
Path,
|
||||
} from 'react-native-svg';
|
||||
|
||||
export default class ArchiveIcon extends PureComponent {
|
||||
static propTypes = {
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {color, height, width} = this.props;
|
||||
|
||||
return (
|
||||
<View style={{height, width, alignItems: 'flex-start'}}>
|
||||
<Svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox='0 0 14 14'
|
||||
>
|
||||
<Path
|
||||
d='M8.5 6.5q0-0.203-0.148-0.352t-0.352-0.148h-2q-0.203 0-0.352 0.148t-0.148 0.352 0.148 0.352 0.352 0.148h2q0.203 0 0.352-0.148t0.148-0.352zM13 5v7.5q0 0.203-0.148 0.352t-0.352 0.148h-11q-0.203 0-0.352-0.148t-0.148-0.352v-7.5q0-0.203 0.148-0.352t0.352-0.148h11q0.203 0 0.352 0.148t0.148 0.352zM13.5 1.5v2q0 0.203-0.148 0.352t-0.352 0.148h-12q-0.203 0-0.352-0.148t-0.148-0.352v-2q0-0.203 0.148-0.352t0.352-0.148h12q0.203 0 0.352 0.148t0.148 0.352z'
|
||||
fill={color}
|
||||
/>
|
||||
</Svg>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user