Compare commits

..

11 Commits

Author SHA1 Message Date
enahum
06aa01507f Release 1.1 changelog (#827)
* Release 1.1 changelog

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

Set supported server version
2017-08-04 16:36:42 -04:00
enahum
60bf695789 Version Bump to 47 (#825) 2017-08-04 11:13:51 -04:00
enahum
24eaef38ef Version Bump to 47 (#824) 2017-08-04 10:56:43 -04:00
Harrison Healey
53ae78674e RN-205/RN-210 Allow tapping buttons on post and DM lists without closing keyboard (#823) 2017-08-04 09:14:57 -04:00
enahum
d7ef19f883 Version Bump to 46 (#822) 2017-08-04 09:09:38 -04:00
enahum
15e05c44f5 Version Bump to 46 (#821) 2017-08-04 09:07:31 -04:00
enahum
12f6b11e09 Fix Create on Create channel to not push another Create Channel screen (#820) 2017-08-03 18:14:44 -04:00
enahum
10fd389e15 Android Version Bump to 45 (#819)
* Updated yarn.lock (#817)

* Version Bump to 45
2017-08-03 18:09:44 -04:00
enahum
e50049d0d6 Version Bump to 45 (#818) 2017-08-03 18:09:36 -04:00
enahum
9d3e072f7a Fastlane ensure branch for cutting builds (#815) 2017-08-03 16:25:58 -04:00
Harrison Healey
537598cc64 Updated yarn.lock 2017-08-03 16:22:30 -04:00
300 changed files with 6868 additions and 21694 deletions

5
.gitignore vendored
View File

@@ -65,11 +65,6 @@ mattermost.keystore
# Sentry
android/sentry.properties
ios/sentry.properties
# Testing
.nyc_output
# Pods
.podinstall
ios/Pods/

View File

@@ -1,70 +1,9 @@
# Mattermost Mobile Apps Changelog
## v1.3 Release
- Release Date: October 5, 2017
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Tablet Support (Beta)
- Added support for landscape view, so the app may be used on tablets
- Note: Tablet support is in beta, and further improvements are planned for a later date
#### Link Previews
- Added support for image, GIF, and youtube link previews
#### Notifications
- Android: Added the ability to set light, vibrate, and sound settings
- Android: Improved notification stacking so most recent notification shows first
- Updated the design for Notification settings to improve usability
- Added the ability to reply from a push notification without opening the app (requires Android v7.0+, iOS 10+)
- Increased speed when opening app from a push notification
#### Download Files
- Added the ability to download all files on Android and images on iOS
### Improvements
- Using `+` shortcut for emoji reactions is now supported
- Improved emoji formatting (alignment and rendering of non-square aspect ratios)
- Added support for error tracking with Sentry
- Only show the "Connecting..." bar after two connection attempts
### Bug Fixes
- Fixed link rendering not working in certain cases
- Fixed theme color issue with status bar on Android
## v1.2 Release
- Release Date: September 5, 2017
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### AppConfig Support for EMM solutions
- Added [AppConfig](https://www.appconfig.org/) support, to make it easier to integrate with a variety of EMM solutions
#### Code block viewer
- Tap on a code block to open a viewer for easier reading
### Improvements
- Updated formatting for markdown lists and code blocks
- Updated formatting for `in:` and `from:` search autocomplete
### Emoji Picker for Emoji Reactions
- Added an emoji picker for selecting a reaction
### Bug Fixes
- Fixed issue where if only LDAP and GitLab login were enabled, LDAP did not show up on the login page
- Fixed issue with 3 digit mention count UI in channel drawer
### Known Issues
- Using `+:emoji:` to react to a message is not yet supported
## v1.1 Release
- Release Date: August 2017
- Server Versions Supported: Server v3.10+ is required, Self-Signed SSL Certificates are not supported
- Server Versions Supported: Server v3.10+ is required, Self-Signed SSL Certificates are not yet supported
### Highlights
@@ -118,7 +57,7 @@
## v1.0 Release
- Release Date: July 10, 2017
- Server Versions Supported: Server v3.8+ is required, Self-Signed SSL Certificates are not supported
- Server Versions Supported: Server v3.8+ is required, Self-Signed SSL Certificates are not yet supported
### Highlights

View File

@@ -1,708 +0,0 @@
package com.imagepicker;
import android.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.MediaStore;
import android.provider.Settings;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StyleRes;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Patterns;
import android.webkit.MimeTypeMap;
import android.content.pm.PackageManager;
import com.facebook.react.ReactActivity;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.imagepicker.media.ImageConfig;
import com.imagepicker.permissions.PermissionUtils;
import com.imagepicker.permissions.OnImagePickerPermissionsCallback;
import com.imagepicker.utils.MediaUtils.ReadExifResult;
import com.imagepicker.utils.RealPathUtil;
import com.imagepicker.utils.UI;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import com.facebook.react.modules.core.PermissionListener;
import com.facebook.react.modules.core.PermissionAwareActivity;
import static com.imagepicker.utils.MediaUtils.*;
import static com.imagepicker.utils.MediaUtils.createNewFile;
import static com.imagepicker.utils.MediaUtils.getResizedImage;
public class ImagePickerModule extends ReactContextBaseJavaModule
implements ActivityEventListener
{
public static final int REQUEST_LAUNCH_IMAGE_CAPTURE = 13001;
public static final int REQUEST_LAUNCH_IMAGE_LIBRARY = 13002;
public static final int REQUEST_LAUNCH_VIDEO_LIBRARY = 13003;
public static final int REQUEST_LAUNCH_VIDEO_CAPTURE = 13004;
public static final int REQUEST_PERMISSIONS_FOR_CAMERA = 14001;
public static final int REQUEST_PERMISSIONS_FOR_LIBRARY = 14002;
private final ReactApplicationContext reactContext;
private final int dialogThemeId;
protected Callback callback;
private ReadableMap options;
protected Uri cameraCaptureURI;
private Boolean noData = false;
private Boolean pickVideo = false;
private ImageConfig imageConfig = new ImageConfig(null, null, 0, 0, 100, 0, false);
@Deprecated
private int videoQuality = 1;
@Deprecated
private int videoDurationLimit = 0;
private ResponseHelper responseHelper = new ResponseHelper();
private PermissionListener listener = new PermissionListener()
{
public boolean onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults)
{
boolean permissionsGranted = true;
for (int i = 0; i < permissions.length; i++)
{
final boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
permissionsGranted = permissionsGranted && granted;
}
if (callback == null || options == null)
{
return false;
}
if (!permissionsGranted)
{
responseHelper.invokeError(callback, "Permissions weren't granted");
return false;
}
switch (requestCode)
{
case REQUEST_PERMISSIONS_FOR_CAMERA:
launchCamera(options, callback);
break;
case REQUEST_PERMISSIONS_FOR_LIBRARY:
launchImageLibrary(options, callback);
break;
}
return true;
}
};
public ImagePickerModule(ReactApplicationContext reactContext,
@StyleRes final int dialogThemeId)
{
super(reactContext);
this.dialogThemeId = dialogThemeId;
this.reactContext = reactContext;
this.reactContext.addActivityEventListener(this);
}
@Override
public String getName() {
return "ImagePickerManager";
}
@ReactMethod
public void showImagePicker(final ReadableMap options, final Callback callback) {
Activity currentActivity = getCurrentActivity();
if (currentActivity == null)
{
responseHelper.invokeError(callback, "can't find current Activity");
return;
}
this.callback = callback;
this.options = options;
imageConfig = new ImageConfig(null, null, 0, 0, 100, 0, false);
final AlertDialog dialog = UI.chooseDialog(this, options, new UI.OnAction()
{
@Override
public void onTakePhoto(@NonNull final ImagePickerModule module)
{
if (module == null)
{
return;
}
module.launchCamera();
}
@Override
public void onUseLibrary(@NonNull final ImagePickerModule module)
{
if (module == null)
{
return;
}
module.launchImageLibrary();
}
@Override
public void onCancel(@NonNull final ImagePickerModule module)
{
if (module == null)
{
return;
}
module.doOnCancel();
}
@Override
public void onCustomButton(@NonNull final ImagePickerModule module,
@NonNull final String action)
{
if (module == null)
{
return;
}
module.invokeCustomButton(action);
}
});
dialog.show();
}
public void doOnCancel()
{
responseHelper.invokeCancel(callback);
}
public void launchCamera()
{
this.launchCamera(this.options, this.callback);
}
// NOTE: Currently not reentrant / doesn't support concurrent requests
@ReactMethod
public void launchCamera(final ReadableMap options, final Callback callback)
{
if (!isCameraAvailable())
{
responseHelper.invokeError(callback, "Camera not available");
return;
}
final Activity currentActivity = getCurrentActivity();
if (currentActivity == null)
{
responseHelper.invokeError(callback, "can't find current Activity");
return;
}
this.options = options;
if (!permissionsCheck(currentActivity, callback, REQUEST_PERMISSIONS_FOR_CAMERA))
{
return;
}
parseOptions(this.options);
int requestCode;
Intent cameraIntent;
if (pickVideo)
{
requestCode = REQUEST_LAUNCH_VIDEO_CAPTURE;
cameraIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, videoQuality);
if (videoDurationLimit > 0)
{
cameraIntent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, videoDurationLimit);
}
}
else
{
requestCode = REQUEST_LAUNCH_IMAGE_CAPTURE;
cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
final File original = createNewFile(reactContext, this.options, false);
imageConfig = imageConfig.withOriginalFile(original);
cameraCaptureURI = RealPathUtil.compatUriFromFile(reactContext, imageConfig.original);
if (cameraCaptureURI == null)
{
responseHelper.invokeError(callback, "Couldn't get file path for photo");
return;
}
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, cameraCaptureURI);
}
if (cameraIntent.resolveActivity(reactContext.getPackageManager()) == null)
{
responseHelper.invokeError(callback, "Cannot launch camera");
return;
}
this.callback = callback;
try
{
currentActivity.startActivityForResult(cameraIntent, requestCode);
}
catch (ActivityNotFoundException e)
{
e.printStackTrace();
responseHelper.invokeError(callback, "Cannot launch camera");
}
}
public void launchImageLibrary()
{
this.launchImageLibrary(this.options, this.callback);
}
// NOTE: Currently not reentrant / doesn't support concurrent requests
@ReactMethod
public void launchImageLibrary(final ReadableMap options, final Callback callback)
{
final Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
responseHelper.invokeError(callback, "can't find current Activity");
return;
}
this.options = options;
if (!permissionsCheck(currentActivity, callback, REQUEST_PERMISSIONS_FOR_LIBRARY))
{
return;
}
parseOptions(this.options);
int requestCode;
Intent libraryIntent;
if (pickVideo)
{
requestCode = REQUEST_LAUNCH_VIDEO_LIBRARY;
libraryIntent = new Intent(Intent.ACTION_PICK);
libraryIntent.setType("video/*");
}
else
{
requestCode = REQUEST_LAUNCH_IMAGE_LIBRARY;
libraryIntent = new Intent(Intent.ACTION_PICK,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
}
if (libraryIntent.resolveActivity(reactContext.getPackageManager()) == null)
{
responseHelper.invokeError(callback, "Cannot launch photo library");
return;
}
this.callback = callback;
try
{
currentActivity.startActivityForResult(libraryIntent, requestCode);
}
catch (ActivityNotFoundException e)
{
e.printStackTrace();
responseHelper.invokeError(callback, "Cannot launch photo library");
}
}
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
//robustness code
if (passResult(requestCode))
{
return;
}
responseHelper.cleanResponse();
// user cancel
if (resultCode != Activity.RESULT_OK)
{
removeUselessFiles(requestCode, imageConfig);
responseHelper.invokeCancel(callback);
callback = null;
return;
}
Uri uri = null;
switch (requestCode)
{
case REQUEST_LAUNCH_IMAGE_CAPTURE:
uri = cameraCaptureURI;
break;
case REQUEST_LAUNCH_IMAGE_LIBRARY:
uri = data.getData();
String realPath = getRealPathFromURI(uri);
final boolean isUrl = !TextUtils.isEmpty(realPath) &&
Patterns.WEB_URL.matcher(realPath).matches();
if (realPath == null || isUrl)
{
try
{
File file = createFileFromURI(uri);
realPath = file.getAbsolutePath();
uri = Uri.fromFile(file);
}
catch (Exception e)
{
// image not in cache
responseHelper.putString("error", "Could not read photo");
responseHelper.putString("uri", uri.toString());
responseHelper.invokeResponse(callback);
callback = null;
return;
}
}
imageConfig = imageConfig.withOriginalFile(new File(realPath));
break;
case REQUEST_LAUNCH_VIDEO_LIBRARY:
responseHelper.putString("uri", data.getData().toString());
responseHelper.putString("path", getRealPathFromURI(data.getData()));
responseHelper.invokeResponse(callback);
callback = null;
return;
case REQUEST_LAUNCH_VIDEO_CAPTURE:
final String path = getRealPathFromURI(data.getData());
responseHelper.putString("uri", data.getData().toString());
responseHelper.putString("path", path);
fileScan(reactContext, path);
responseHelper.invokeResponse(callback);
callback = null;
return;
}
final ReadExifResult result = readExifInterface(responseHelper, imageConfig);
if (result.error != null)
{
removeUselessFiles(requestCode, imageConfig);
responseHelper.invokeError(callback, result.error.getMessage());
callback = null;
return;
}
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imageConfig.original.getAbsolutePath(), options);
int initialWidth = options.outWidth;
int initialHeight = options.outHeight;
updatedResultResponse(uri, imageConfig.original.getAbsolutePath());
// don't create a new file if contraint are respected
if (imageConfig.useOriginal(initialWidth, initialHeight, result.currentRotation))
{
responseHelper.putInt("width", initialWidth);
responseHelper.putInt("height", initialHeight);
fileScan(reactContext, imageConfig.original.getAbsolutePath());
}
else
{
imageConfig = getResizedImage(reactContext, this.options, imageConfig, initialWidth, initialHeight, requestCode);
if (imageConfig.resized == null)
{
removeUselessFiles(requestCode, imageConfig);
responseHelper.putString("error", "Can't resize the image");
}
else
{
uri = Uri.fromFile(imageConfig.resized);
BitmapFactory.decodeFile(imageConfig.resized.getAbsolutePath(), options);
responseHelper.putInt("width", options.outWidth);
responseHelper.putInt("height", options.outHeight);
updatedResultResponse(uri, imageConfig.resized.getAbsolutePath());
fileScan(reactContext, imageConfig.resized.getAbsolutePath());
}
}
if (imageConfig.saveToCameraRoll && requestCode == REQUEST_LAUNCH_IMAGE_CAPTURE)
{
final RolloutPhotoResult rolloutResult = rolloutPhotoFromCamera(imageConfig);
if (rolloutResult.error == null)
{
imageConfig = rolloutResult.imageConfig;
uri = Uri.fromFile(imageConfig.getActualFile());
updatedResultResponse(uri, imageConfig.getActualFile().getAbsolutePath());
}
else
{
removeUselessFiles(requestCode, imageConfig);
final String errorMessage = new StringBuilder("Error moving image to camera roll: ")
.append(rolloutResult.error.getMessage()).toString();
responseHelper.putString("error", errorMessage);
return;
}
}
responseHelper.invokeResponse(callback);
callback = null;
this.options = null;
}
public void invokeCustomButton(@NonNull final String action)
{
responseHelper.invokeCustomButton(this.callback, action);
}
@Override
public void onNewIntent(Intent intent) { }
public Context getContext()
{
return getReactApplicationContext();
}
public @StyleRes int getDialogThemeId()
{
return this.dialogThemeId;
}
public @NonNull Activity getActivity()
{
return getCurrentActivity();
}
private boolean passResult(int requestCode)
{
return callback == null || (cameraCaptureURI == null && requestCode == REQUEST_LAUNCH_IMAGE_CAPTURE)
|| (requestCode != REQUEST_LAUNCH_IMAGE_CAPTURE && requestCode != REQUEST_LAUNCH_IMAGE_LIBRARY
&& requestCode != REQUEST_LAUNCH_VIDEO_LIBRARY && requestCode != REQUEST_LAUNCH_VIDEO_CAPTURE);
}
private void updatedResultResponse(@Nullable final Uri uri,
@NonNull final String path)
{
responseHelper.putString("uri", uri.toString());
responseHelper.putString("path", path);
if (!noData) {
responseHelper.putString("data", getBase64StringFromFile(path));
}
putExtraFileInfo(path, responseHelper);
}
private boolean permissionsCheck(@NonNull final Activity activity,
@NonNull final Callback callback,
@NonNull final int requestCode)
{
final int writePermission = ActivityCompat
.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);
final int cameraPermission = ActivityCompat
.checkSelfPermission(activity, Manifest.permission.CAMERA);
final boolean permissionsGrated = writePermission == PackageManager.PERMISSION_GRANTED &&
cameraPermission == PackageManager.PERMISSION_GRANTED;
if (!permissionsGrated)
{
final Boolean dontAskAgain = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) && ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA);
if (dontAskAgain)
{
final AlertDialog dialog = PermissionUtils
.explainingDialog(this, options, new PermissionUtils.OnExplainingPermissionCallback()
{
@Override
public void onCancel(WeakReference<ImagePickerModule> moduleInstance,
DialogInterface dialogInterface)
{
final ImagePickerModule module = moduleInstance.get();
if (module == null)
{
return;
}
module.doOnCancel();
}
@Override
public void onReTry(WeakReference<ImagePickerModule> moduleInstance,
DialogInterface dialogInterface)
{
final ImagePickerModule module = moduleInstance.get();
if (module == null)
{
return;
}
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", module.getContext().getPackageName(), null);
intent.setData(uri);
final Activity innerActivity = module.getActivity();
if (innerActivity == null)
{
return;
}
innerActivity.startActivityForResult(intent, 1);
}
});
dialog.show();
return false;
}
else
{
String[] PERMISSIONS = {Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA};
if (activity instanceof ReactActivity)
{
((ReactActivity) activity).requestPermissions(PERMISSIONS, requestCode, listener);
}
else if (activity instanceof OnImagePickerPermissionsCallback)
{
((OnImagePickerPermissionsCallback) activity).setPermissionListener(listener);
ActivityCompat.requestPermissions(activity, PERMISSIONS, requestCode);
}
else if (activity instanceof PermissionAwareActivity) {
((PermissionAwareActivity) activity).requestPermissions(PERMISSIONS, requestCode, listener);
}
else
{
final String errorDescription = new StringBuilder(activity.getClass().getSimpleName())
.append(" must implement ")
.append(OnImagePickerPermissionsCallback.class.getSimpleName())
.append(PermissionAwareActivity.class.getSimpleName())
.toString();
throw new UnsupportedOperationException(errorDescription);
}
return false;
}
}
return true;
}
private boolean isCameraAvailable() {
return reactContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)
|| reactContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
}
private @NonNull String getRealPathFromURI(@NonNull final Uri uri) {
return RealPathUtil.getRealPathFromURI(reactContext, uri);
}
/**
* Create a file from uri to allow image picking of image in disk cache
* (Exemple: facebook image, google image etc..)
*
* @doc =>
* https://github.com/nostra13/Android-Universal-Image-Loader#load--display-task-flow
*
* @param uri
* @return File
* @throws Exception
*/
private File createFileFromURI(Uri uri) throws Exception {
File file = new File(reactContext.getExternalCacheDir(), "photo-" + uri.getLastPathSegment());
InputStream input = reactContext.getContentResolver().openInputStream(uri);
OutputStream output = new FileOutputStream(file);
try {
byte[] buffer = new byte[4 * 1024];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
output.flush();
} finally {
output.close();
input.close();
}
return file;
}
private String getBase64StringFromFile(String absoluteFilePath) {
InputStream inputStream = null;
try {
inputStream = new FileInputStream(new File(absoluteFilePath));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
byte[] bytes;
byte[] buffer = new byte[8192];
int bytesRead;
ByteArrayOutputStream output = new ByteArrayOutputStream();
try {
while ((bytesRead = inputStream.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
bytes = output.toByteArray();
return Base64.encodeToString(bytes, Base64.NO_WRAP);
}
private void putExtraFileInfo(@NonNull final String path,
@NonNull final ResponseHelper responseHelper)
{
// size && filename
try {
File f = new File(path);
responseHelper.putDouble("fileSize", f.length());
responseHelper.putString("fileName", f.getName());
} catch (Exception e) {
e.printStackTrace();
}
// type
String extension = MimeTypeMap.getFileExtensionFromUrl(path);
if (extension != null) {
responseHelper.putString("type", MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension));
}
}
private void parseOptions(final ReadableMap options) {
noData = false;
if (options.hasKey("noData")) {
noData = options.getBoolean("noData");
}
imageConfig = imageConfig.updateFromOptions(options);
pickVideo = false;
if (options.hasKey("mediaType") && options.getString("mediaType").equals("video")) {
pickVideo = true;
}
videoQuality = 1;
if (options.hasKey("videoQuality") && options.getString("videoQuality").equals("low")) {
videoQuality = 0;
}
videoDurationLimit = 0;
if (options.hasKey("durationLimit")) {
videoDurationLimit = options.getInt("durationLimit");
}
}
}

148
Makefile
View File

@@ -5,44 +5,32 @@
ios_target := $(filter-out build-ios,$(MAKECMDGOALS))
android_target := $(filter-out build-android,$(MAKECMDGOALS))
POD := $(shell command -v pod 2> /dev/null)
.yarninstall: package.json
@if ! [ $(shell command -v yarn 2> /dev/null) ]; then \
@echo "yarn is not installed https://yarnpkg.com"; \
echo "yarn is not installed https://yarnpkg.com"; \
exit 1; \
fi
@echo Getting Javascript dependencies
@yarn install --pure-lockfile
@echo Getting dependencies using yarn
@touch $@
yarn install --pure-lockfile
.podinstall:
ifdef POD
@echo Getting Cocoapods dependencies;
@cd ios && pod install;
else
@echo "Cocoapods is not installed https://cocoapods.org/"
@exit 1
endif
@touch $@
touch $@
BASE_ASSETS = $(shell find assets/base -type d) $(shell find assets/base -type f -name '*')
OVERRIDE_ASSETS = $(shell find assets/override -type d 2> /dev/null) $(shell find assets/override -type f -name '*' 2> /dev/null)
dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
@mkdir -p dist
mkdir -p dist
@if [ -e dist/assets ] ; then \
rm -rf dist/assets; \
fi
@echo "Generating app assets"
@node scripts/make-dist-assets.js
node scripts/make-dist-assets.js
pre-run: | .yarninstall .podinstall dist/assets
pre-run: .yarninstall dist/assets
run: run-ios
@@ -50,77 +38,71 @@ start: | pre-run start-packager
stop: stop-packager
check-device-ios:
run-ios: | start
@if ! [ $(shell command -v xcodebuild) ]; then \
@echo "xcode is not installed"; \
@exit 1; \
echo "xcode is not installed"; \
exit 1; \
fi
@if ! [ $(shell command -v watchman) ]; then \
@echo "watchman is not installed"; \
@exit 1; \
echo "watchman is not installed"; \
exit 1; \
fi
run-ios: | check-device-ios start
@echo Running iOS app in development
@react-native run-ios --simulator="${SIMULATOR}"
check-device-android:
npm run run-ios
open -a Simulator
run-android: | start prepare-android-build
@if ! [ $(ANDROID_HOME) ]; then \
@echo "ANDROID_HOME is not set"; \
@exit 1; \
echo "ANDROID_HOME is not set"; \
exit 1; \
fi
@if ! [ $(shell command -v adb 2> /dev/null) ]; then \
@echo "adb is not installed"; \
@exit 1; \
echo "adb is not installed"; \
exit 1; \
fi
ifneq ($(shell adb get-state),device)
@echo "no android device or emulator is running"
@exit 1;
endif
ifneq ($(shell adb get-state),device)
echo "no android device or emulator is running"
exit 1;
endif
@if ! [ $(shell command -v watchman 2> /dev/null) ]; then \
@echo "watchman is not installed"; \
@exit 1; \
echo "watchman is not installed"; \
exit 1; \
fi
run-android: | check-device-android start prepare-android-build
@echo Running Android app in development
@react-native run-android --no-packager
npm run run-android
test: pre-run
@yarn test
npm test
check-style: .yarninstall
@echo Checking for style guide compliance
@node_modules/.bin/eslint --ext \".js\" --ignore-pattern node_modules --quiet .
npm run check
clean:
@echo Cleaning started
@echo Cleaning app
@yarn cache clean
@rm -rf node_modules
@rm -f .yarninstall
@rm -f .podinstall
@rm -rf dist
@rm -rf ios/build
@rm -rf ios/Pods
@rm -rf android/app/build
@echo Cleanup finished
yarn cache clean
rm -rf node_modules
rm -f .yarninstall
rm -rf dist
rm -rf ios/build
rm -rf android/app/build
post-install:
@./node_modules/.bin/remotedev-debugger --hostname localhost --port 5678 --injectserver
./node_modules/.bin/remotedev-debugger --hostname localhost --port 5678 --injectserver
@# Must remove the .babelrc for 0.42.0 to work correctly
@# Need to copy custom ImagePickerModule.java that implements correct permission checks for android
@rm node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModule.java
@cp ./ImagePickerModule.java node_modules/react-native-image-picker/android/src/main/java/com/imagepicker
@rm -f node_modules/intl/.babelrc
rm -f node_modules/intl/.babelrc
@# Hack to get react-intl and its dependencies to work with react-native
@# Based off of https://github.com/este/este/blob/master/gulp/native-fix.js
@sed -i'' -e 's|"./locale-data/index.js": false|"./locale-data/index.js": "./locale-data/index.js"|g' node_modules/react-intl/package.json
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-messageformat/package.json
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
@sed -i'' -e 's|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_SENSOR);|g' node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/params/Orientation.java
sed -i'' -e 's|"./locale-data/index.js": false|"./locale-data/index.js": "./locale-data/index.js"|g' node_modules/react-intl/package.json
sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-messageformat/package.json
sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json
sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
start-packager:
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
@@ -135,20 +117,11 @@ stop-packager:
@echo Stopping React Native packager server
@if [ -e "server.PID" ] ; then \
kill -9 `cat server.PID` && rm server.PID; \
echo React Native packager server stopped; \
else \
echo No React Native packager server running; \
fi
check-ios-target:
ifeq ($(ios_target), )
@echo No target set to build iOS app
@echo "Try running make build-ios TARGET where TARGET is one of dev, beta or release"
@exit 1
endif
ifneq ($(ios_target), $(filter $(ios_target),dev beta release))
@echo Invalid target set to build iOS app
@echo "Try running make build-ios TARGET where TARGET is one of dev, beta or release"
ifneq ($(ios_target), $(filter $(ios_target), dev beta release))
@echo "Try running make build-ios TARGET\nWhere TARGET is one of dev, beta or release"
@exit 1
endif
@@ -160,14 +133,8 @@ do-build-ios:
build-ios: | check-ios-target pre-run check-style start-packager do-build-ios stop-packager
check-android-target:
ifeq ($(android_target), )
@echo No target set to build Android app
@echo "Try running make build-android TARGET where TARGET is one of dev, beta or release"
@exit 1
endif
ifneq ($(android_target), $(filter $(android_target),dev alpha release))
@echo Invalid target set to build Android app
@echo "Try running make build-android TARGET where TARGET is one of dev, beta or release"
ifneq ($(android_target), $(filter $(android_target), dev alpha release))
@echo "Try running make build-android TARGET\nWhere TARGET is one of dev, beta or release"
@exit 1
endif
@@ -175,6 +142,7 @@ prepare-android-build:
@rm -rf ./node_modules/react-native/local-cli/templates/HelloWorld
@rm -rf ./node_modules/react-native-linear-gradient/Examples/
@rm -rf ./node_modules/react-native-orientation/demo/
@cd android && ./gradlew clean
do-build-android:
@echo "Building android $(android_target) app"
@@ -182,24 +150,6 @@ do-build-android:
build-android: | check-android-target pre-run check-style start-packager prepare-android-build do-build-android stop-packager
do-unsigned-ios:
@echo "Building unsigned iOS app"
@cd fastlane && NODE_ENV=production bundle exec fastlane ios unsigned
@mkdir -p build-ios
@cd ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -sdk iphoneos -configuration Relase -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
@cd build-ios/ && mkdir -p Payload && cp -R Build/Products/Release-iphoneos/Mattermost.app Payload/ && zip -r Mattermost-unsigned.ipa Payload/
@mv build-ios/Mattermost-unsigned.ipa .
@rm -rf build-ios/
do-unsigned-android:
@echo "Building unsigned Android app"
@cd fastlane && NODE_ENV=production bundle exec fastlane android unsigned
@mv android/app/build/outputs/apk/app-unsigned-unsigned.apk ./Mattermost-unsigned.apk
unsigned-android: pre-run check-style start-packager do-unsigned-android stop-packager
unsigned-ios: pre-run check-style start-packager do-unsigned-ios stop-packager
alpha:
@:

View File

@@ -8,6 +8,39 @@ This document includes a list of open source components used in Mattermost Mobil
--------
## react-native-swiper
This product contains 'react-native-swiper', A Swiper component for React Native.
* HOMEPAGE:
* https://github.com/leecade/react-native-swiper
* LICENSE:
The MIT License (MIT)
Copyright (c) 2015 斯人
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-tooltip
This product contains a modified portion of 'react-native-tooltip', A react-native component from displaying tooltip. Uses UIMenuController.
@@ -1090,325 +1123,3 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## analytics-react-native
This product contains 'analytics-react-native', A React Native client for Segment. The hassle-free way to integrate analytics into any application.
* HOMEPAGE:
* https://github.com/neiker/analytics-react-native
* LICENSE:
MIT License
Copyright (c) 2016 Javier Alvarez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-passcode-status
This product contains 'react-native-passcode-status', A thin wrapper around UIDevice-PasscodeStatus.
* HOMEPAGE:
* https://github.com/tradle/react-native-passcode-status
* LICENSE:
The MIT License (MIT)
Copyright (c) 2015 Tradle
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-local-auth
This product contains 'react-native-local-auth', Authenticate users with Touch ID, with optional fallback to passcode.
* HOMEPAGE:
* https://github.com/tradle/react-native-local-auth
* LICENSE:
ISC License
Copyright (c) 2015, [Tradle, Inc](http://tradle.io/)
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---
## jail-monkey
This product contains 'jail-monkey', Identify if a phone has been jail-broken or rooted for iOS/Android.
* HOMEPAGE:
* https://github.com/GantMan/jail-monkey/
* LICENSE:
MIT License
Copyright (c) 2017 Gant Laborde
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-fast-image
FastImage, performant React Native image component.
* HOMEPAGE:
* https://github.com/DylanVann/react-native-fast-image
* LICENSE:
MIT License
Copyright (c) 2017 Dylan Vann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-circular-progress
React Native component for creating animated, circular progress with ReactART
* HOMEPAGE:
* https://github.com/bgryszko/react-native-circular-progress
* LICENSE:
The MIT License (MIT)
Copyright (c) 2015 Bart Gryszko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
## react-native-fetch-blob
A project committed to making file access and data transfer easier, efficient for React Native developers.
* HOMEPAGE:
* https://github.com/wkh237/react-native-fetch-blob#user-content-upload-example--dropbox-files-upload-api
* LICENSE:
MIT License
Copyright (c) 2017 xeiyan@gmail.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-sentry
This product contains 'react-native-sentry', the Sentry SDK for React Native
* HOMEPAGE:
* https://github.com/getsentry/react-native-sentry
* LICENSE:
The MIT License (MIT)
Copyright (c) 2017 Sentry
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
---
## react-native-youtube
This product contains 'react-native-youtube', A <YouTube /> component for React Native.
* HOMEPAGE:
* https://github.com/inProgress-team/react-native-youtube
* LICENSE:
The MIT License (MIT)
Copyright (c) 2015 Param Aggarwal
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## youtube-video-id
This product contains 'youtube-video-id', Extracts the YouTube video ID from a url or string.
* HOMEPAGE:
* https://github.com/remarkablemark/youtube-video-id
* LICENSE:
MIT License
Copyright (c) 2016 Menglin "Mark" Xu <mark@remarkablemark.org>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,9 +1,6 @@
# Mattermost Mobile
**Supported Server Versions:** 4.0+
**Supported iOS versions:** 9.3+
**Supported Android versions:** 5.0+
**Supported Server Versions:** 3.8+
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 11 languages. Learn more at https://mattermost.com.
@@ -135,11 +132,9 @@ We plan to add support for tablets in the future, but the timeline depends on ho
### I keep getting a message "Cannot connect to the server. Please check your server URL and internet connection."
This sometimes appears when there is an issue with the SSL certitificate configuration.
Our second generation mobile apps only support server versions 3.8+. If your server version is too old, you might see this error message come up.
To check that your SSL certificate is set up correctly, test the SSL certificate by visiting a site such as https://www.ssllabs.com/ssltest/index.html. If theres an error about the missing chain or certificate path, there is likely an intermediate certificate missing that needs to be included.
Please note that the apps cannot connect to servers with self-signed certificates, consider using [Let's Encrypt](https://docs.mattermost.com/install/config-ssl-http2-nginx.html) instead.
To check your server version, log on to the site on desktop and go to Main Menu > About Mattermost.
### I see a “Connecting…” bar that does not go away
@@ -148,11 +143,3 @@ If your app is working properly, you should see a grey “Connecting…” bar t
If you are seeing this message all the time, and your internet connection seems fine:
Ask your server administrator if the server uses NGINX or another webserver as a reverse proxy. If so, they should check that it is configured correctly for [supporting the websocket connection for APIv4 endpoints](https://docs.mattermost.com/install/install-ubuntu-1604.html#configuring-nginx-as-a-proxy-for-mattermost-server).
# Issues building app for own device using make build-*
That command is an internal pipeline command for mattermost mobile to publish the mobile apps to ````Apple App Store```` and ````Google Play Store````. All ````make build-*```` commands should be avoided for this reason.
To build the modified react native client use the instructions for [Running on Device](http://facebook.github.io/react-native/docs/running-on-device.html) from the [React Native Guide](https://facebook.github.io/react-native/docs/getting-started.html).

View File

@@ -68,10 +68,6 @@ import com.android.build.OutputFile
apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
if (System.getenv("MM_SENTRY_ENABLED") == "true") {
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
}
/**
* Set this to true to create two separate APKs instead of one:
* - An APK that only works on ARM devices
@@ -93,10 +89,10 @@ android {
defaultConfig {
applicationId "com.mattermost.rnbeta"
minSdkVersion 21
minSdkVersion 16
targetSdkVersion 23
versionCode 57
versionName "1.3.0"
versionCode 47
versionName "1.1.0"
multiDexEnabled true
ndk {
abiFilters "armeabi-v7a", "x86"
@@ -131,10 +127,6 @@ android {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
unsigned.initWith(buildTypes.release)
unsigned {
signingConfig null
}
}
// applicationVariants are e.g. debug, release
applicationVariants.all { variant ->
@@ -168,19 +160,6 @@ dependencies {
compile project(':react-native-linear-gradient')
compile project(':react-native-vector-icons')
compile project(':react-native-svg')
compile project(':react-native-local-auth')
compile project(':jail-monkey')
compile project(':react-native-youtube')
compile project(':react-native-sentry')
compile project(':react-native-exception-handler')
compile project(':react-native-fetch-blob')
// For animated GIF support
compile 'com.facebook.fresco:animated-base-support:1.3.0'
// For WebP support, including animated WebP
compile 'com.facebook.fresco:animated-gif:1.3.0'
compile 'com.facebook.fresco:animated-webp:1.3.0'
compile 'com.facebook.fresco:webpsupport:1.3.0'
}
// Run this once to be able to run the application with BUCK

View File

@@ -14,7 +14,6 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="com.google.android.c2dm.permission.SEND" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -30,10 +29,7 @@
android:icon="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
>
<meta-data android:name="android.content.APP_RESTRICTIONS"
android:resource="@xml/app_restrictions" />
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\0"/>
<meta-data android:name="com.wix.reactnativenotifications.gcmSenderId" android:value="184930218130\0"/>
<activity
android:name=".MainActivity"
android:label="@string/app_name"
@@ -43,19 +39,10 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<action android:name="android.intent.action.DOWNLOAD_COMPLETE"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<service android:name=".NotificationDismissService"
android:enabled="true"
android:exported="false" />
<service android:name=".NotificationReplyService"
android:enabled="true"
android:exported="false" />
<activity
android:name="com.reactnativenavigation.controllers.NavigationActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"/>
<receiver android:name=".NotificationDismissReceiver" />
</application>
</manifest>

View File

@@ -7,28 +7,19 @@ import android.content.res.Resources;
import android.content.pm.ApplicationInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.AudioManager;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Build;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.RemoteInput;
import java.util.LinkedHashMap;
import java.util.ArrayList;
import java.lang.reflect.Field;
import com.wix.reactnativenotifications.core.notification.PushNotification;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
import com.wix.reactnativenotifications.core.AppLaunchHelper;
import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import com.wix.reactnativenotifications.core.JsIOHelper;
import com.wix.reactnativenotifications.helpers.ApplicationBadgeHelper;
import android.util.Log;
import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_NAME;
public class CustomPushNotification extends PushNotification {
@@ -36,25 +27,18 @@ public class CustomPushNotification extends PushNotification {
public static final int MESSAGE_NOTIFICATION_ID = 435345;
public static final String GROUP_KEY_MESSAGES = "mm_group_key_messages";
public static final String NOTIFICATION_ID = "notificationId";
public static final String KEY_TEXT_REPLY = "CAN_REPLY";
public static final String NOTIFICATION_REPLIED_EVENT_NAME = "notificationReplied";
private static LinkedHashMap<String,Integer> channelIdToNotificationCount = new LinkedHashMap<String,Integer>();
private static LinkedHashMap<String,ArrayList<Bundle>> channelIdToNotification = new LinkedHashMap<String,ArrayList<Bundle>>();
private static AppLifecycleFacade lifecycleFacade;
private static Context context;
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
this.context = context;
}
public static void clearNotification(int notificationId, String channelId) {
public static void clearNotification(int notificationId) {
if (notificationId != -1) {
String channelId = String.valueOf(notificationId);
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
}
@@ -76,11 +60,11 @@ public class CustomPushNotification extends PushNotification {
Object bundleArray = channelIdToNotification.get(channelId);
ArrayList list = null;
if (bundleArray == null) {
list = new ArrayList(0);
list = new ArrayList();
} else {
list = (ArrayList)bundleArray;
}
list.add(0, data);
list.add(data);
channelIdToNotification.put(channelId, list);
}
@@ -105,13 +89,7 @@ public class CustomPushNotification extends PushNotification {
@Override
protected void postNotification(int id, Notification notification) {
boolean force = false;
Bundle bundle = notification.extras;
if (bundle != null) {
force = bundle.getBoolean("localTest");
}
if (!mAppLifecycleFacade.isAppVisible() || force) {
if (!mAppLifecycleFacade.isAppVisible()) {
super.postNotification(id, notification);
}
}
@@ -120,10 +98,9 @@ public class CustomPushNotification extends PushNotification {
protected Notification.Builder getNotificationBuilder(PendingIntent intent) {
final Resources res = mContext.getResources();
String packageName = mContext.getPackageName();
NotificationPreferences notificationPreferences = NotificationPreferences.getInstance(mContext);
// First, get a builder initialized with defaults from the core class.
final Notification.Builder notification = new Notification.Builder(mContext);
final Notification.Builder notification = super.getNotificationBuilder(intent);
Bundle bundle = mNotificationProps.asBundle();
String title = bundle.getString("title");
if (title == null) {
@@ -131,20 +108,14 @@ public class CustomPushNotification extends PushNotification {
title = mContext.getPackageManager().getApplicationLabel(appInfo).toString();
}
int notificationId = bundle.getString("channel_id").hashCode();
String channelId = bundle.getString("channel_id");
String postId = bundle.getString("post_id");
int notificationId = channelId != null ? channelId.hashCode() : MESSAGE_NOTIFICATION_ID;
String message = bundle.getString("message");
String subText = bundle.getString("subText");
String numberString = bundle.getString("badge");
String smallIcon = bundle.getString("smallIcon");
String largeIcon = bundle.getString("largeIcon");
Bundle b = bundle.getBundle("userInfo");
if (b != null) {
notification.addExtras(b);
}
int smallIconResId;
int largeIconResId;
@@ -172,15 +143,17 @@ public class CustomPushNotification extends PushNotification {
ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), Integer.parseInt(numberString));
}
int numMessages = getMessageCountInChannel(channelId);
int numMessages = 0;
Object objCount = channelIdToNotificationCount.get(channelId);
if (objCount != null) {
numMessages = (Integer)objCount;
}
notification
.setContentIntent(intent)
.setGroupSummary(true)
.setSmallIcon(smallIconResId)
.setVisibility(Notification.VISIBILITY_PRIVATE)
.setPriority(Notification.PRIORITY_HIGH)
.setAutoCancel(true);
.setPriority(Notification.PRIORITY_HIGH);
if (numMessages == 1) {
notification
@@ -193,52 +166,26 @@ public class CustomPushNotification extends PushNotification {
Notification.InboxStyle style = new Notification.InboxStyle();
ArrayList<Bundle> list = (ArrayList<Bundle>) channelIdToNotification.get(channelId);
for (Bundle data : list){
String msg = data.getString("message");
if (msg != message) {
style.addLine(data.getString("message"));
}
style.addLine(data.getString("message"));
}
style.setBigContentTitle(message)
.setSummaryText(String.format("+%d more", (numMessages - 1)));
style.setBigContentTitle(title);
notification.setStyle(style)
.setContentTitle(summaryTitle);
// .setNumber(numMessages);
}
// Let's add a delete intent when the notification is dismissed
Intent delIntent = new Intent(mContext, NotificationDismissService.class);
Intent delIntent = new Intent(mContext, NotificationDismissReceiver.class);
delIntent.putExtra(NOTIFICATION_ID, notificationId);
PendingIntent deleteIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, delIntent, mNotificationProps);
PendingIntent deleteIntent = PendingIntent.getBroadcast(mContext, 0, delIntent, 0);
notification.setDeleteIntent(deleteIntent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
notification.setGroup(GROUP_KEY_MESSAGES);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && postId != null) {
Intent replyIntent = new Intent(mContext, NotificationReplyService.class);
replyIntent.setAction(KEY_TEXT_REPLY);
replyIntent.putExtra(NOTIFICATION_ID, notificationId);
replyIntent.putExtra("pushNotification", bundle);
PendingIntent replyPendingIntent = PendingIntent.getService(mContext, 103, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY)
.setLabel("Reply")
.build();
Notification.Action replyAction = new Notification.Action.Builder(
R.drawable.ic_notif_action_reply, "Reply", replyPendingIntent)
.addRemoteInput(remoteInput)
.setAllowGeneratedReplies(true)
.build();
notification
.setShowWhen(true)
.addAction(replyAction);
}
Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
if (largeIconResId != 0 && (largeIcon != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) {
notification.setLargeIcon(largeIconBitmap);
@@ -248,27 +195,6 @@ public class CustomPushNotification extends PushNotification {
notification.setSubText(subText);
}
String soundUri = notificationPreferences.getNotificationSound();
if (soundUri != null) {
if (soundUri != "none") {
notification.setSound(Uri.parse(soundUri), AudioManager.STREAM_NOTIFICATION);
}
} else {
Uri defaultUri = RingtoneManager.getActualDefaultRingtoneUri(mContext, RingtoneManager.TYPE_NOTIFICATION);
notification.setSound(defaultUri, AudioManager.STREAM_NOTIFICATION);
}
boolean vibrate = notificationPreferences.getShouldVibrate();
if (vibrate) {
// Each element then alternates between delay, vibrate, sleep, vibrate, sleep
notification.setVibrate(new long[] {1000, 1000, 500, 1000, 500});
}
boolean blink = notificationPreferences.getShouldBlink();
if (blink) {
notification.setLights(Color.CYAN, 500, 500);
}
return notification;
}
@@ -276,15 +202,6 @@ public class CustomPushNotification extends PushNotification {
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
}
public static Integer getMessageCountInChannel(String channelId) {
Object objCount = channelIdToNotificationCount.get(channelId);
if (objCount != null) {
return (Integer)objCount;
}
return 1;
}
private void cancelNotification(Bundle data, int notificationId) {
final String channelId = data.getString("channel_id");

View File

@@ -1,5 +1,9 @@
package com.mattermost.rnbeta;
import com.github.yamill.orientation.OrientationPackage;
import com.psykar.cookiemanager.CookieManagerPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.reactnativenavigation.controllers.SplashActivity;
import java.lang.ref.WeakReference;
@@ -33,23 +37,23 @@ public class MainActivity extends SplashActivity {
@Override
public LinearLayout createSplashLayout() {
wr_activity = new WeakReference<>(this);
LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
Context context = getActivity();
final int drawableId = getImageId();
NotificationsLifecycleFacade.getInstance().LoadManagedConfig(getActivity());
Context context = getActivity();
imageView = new ImageView(context);
imageView.setImageResource(drawableId);
LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
imageView.setLayoutParams(layoutParams);
imageView.setScaleType(ImageView.ScaleType.CENTER);
LinearLayout view = new LinearLayout(this);
view.setBackgroundColor(Color.parseColor("#FFFFFF"));
view.setGravity(Gravity.CENTER);
view.addView(imageView);
view.addView(imageView);
return view;
}

View File

@@ -1,17 +1,12 @@
package com.mattermost.rnbeta;
import android.app.Application;
import android.util.Log;
import android.support.annotation.NonNull;
import android.content.Context;
import android.os.Bundle;
import com.facebook.react.ReactApplication;
import com.inprogress.reactnativeyoutube.ReactNativeYouTube;
import io.sentry.RNSentryPackage;
import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage;
import com.RNFetchBlob.RNFetchBlobPackage;
import com.gantix.JailMonkey.JailMonkeyPackage;
import io.tradle.react.LocalAuthPackage;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
@@ -38,7 +33,8 @@ import java.util.Arrays;
import java.util.List;
public class MainApplication extends NavigationApplication implements INotificationsApplication {
public NotificationsLifecycleFacade notificationsLifecycleFacade;
NotificationsLifecycleFacade notificationsLifecycleFacade;
@Override
public boolean isDebug() {
@@ -59,26 +55,20 @@ public class MainApplication extends NavigationApplication implements INotificat
new SvgPackage(),
new LinearGradientPackage(),
new OrientationPackage(),
new RNNotificationsPackage(this),
new LocalAuthPackage(),
new JailMonkeyPackage(),
new RNFetchBlobPackage(),
new MattermostPackage(this),
new RNSentryPackage(this),
new ReactNativeExceptionHandlerPackage(),
new ReactNativeYouTube()
new RNNotificationsPackage(MainApplication.this)
);
}
@Override
public void onCreate() {
super.onCreate();
instance = this;
// Create an object of the custom facade impl
notificationsLifecycleFacade = NotificationsLifecycleFacade.getInstance();
notificationsLifecycleFacade = new NotificationsLifecycleFacade();
// Attach it to react-native-navigation
setActivityCallbacks(notificationsLifecycleFacade);
SoLoader.init(this, /* native exopackage */ false);
}

View File

@@ -1,65 +0,0 @@
package com.mattermost.rnbeta;
import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
public class MattermostManagedModule extends ReactContextBaseJavaModule {
private static MattermostManagedModule instance;
private boolean shouldBlurAppScreen = false;
private MattermostManagedModule(ReactApplicationContext reactContext) {
super(reactContext);
}
public static MattermostManagedModule getInstance(ReactApplicationContext reactContext) {
if (instance == null) {
instance = new MattermostManagedModule(reactContext);
}
return instance;
}
public static MattermostManagedModule getInstance() {
return instance;
}
@Override
public String getName() {
return "MattermostManaged";
}
@ReactMethod
public void blurAppScreen(boolean enabled) {
shouldBlurAppScreen = enabled;
}
public boolean isBlurAppScreenEnabled() {
return shouldBlurAppScreen;
}
@ReactMethod
public void getConfig(final Promise promise) {
try {
Bundle config = NotificationsLifecycleFacade.getInstance().getManagedConfig();
if (config != null) {
Object result = Arguments.fromBundle(config);
promise.resolve(result);
} else {
throw new Exception("The MDM vendor has not sent any Managed configuration");
}
} catch (Exception e) {
promise.reject("no managed configuration", e);
}
}
}

View File

@@ -1,32 +0,0 @@
package com.mattermost.rnbeta;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.bridge.JavaScriptModule;
public class MattermostPackage implements ReactPackage {
private final MainApplication mApplication;
public MattermostPackage(MainApplication application) {
mApplication = application;
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(
MattermostManagedModule.getInstance(reactContext),
NotificationPreferencesModule.getInstance(mApplication, reactContext)
);
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,14 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.content.Intent;
import android.content.BroadcastReceiver;
public class NotificationDismissReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
CustomPushNotification.clearNotification(notificationId);
}
}

View File

@@ -1,23 +0,0 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.content.Intent;
import android.app.IntentService;
import android.os.Bundle;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationDismissService extends IntentService {
public NotificationDismissService() {
super("notificationDismissService");
}
@Override
protected void onHandleIntent(Intent intent) {
Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
String channelId = bundle.getString("channel_id");
CustomPushNotification.clearNotification(notificationId, channelId);
}
}

View File

@@ -1,57 +0,0 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.content.SharedPreferences;
public class NotificationPreferences {
private static NotificationPreferences instance;
public final String SHARED_NAME = "NotificationPreferences";
public final String SOUND_PREF = "NotificationSound";
public final String VIBRATE_PREF = "NotificationVibrate";
public final String BLINK_PREF = "NotificationLights";
private SharedPreferences mSharedPreferences;
private NotificationPreferences(Context context) {
mSharedPreferences = context.getSharedPreferences(SHARED_NAME, Context.MODE_PRIVATE);
}
public static NotificationPreferences getInstance(Context context) {
if (instance == null) {
instance = new NotificationPreferences(context);
}
return instance;
}
public String getNotificationSound() {
return mSharedPreferences.getString(SOUND_PREF, null);
}
public boolean getShouldVibrate() {
return mSharedPreferences.getBoolean(VIBRATE_PREF, true);
}
public boolean getShouldBlink() {
return mSharedPreferences.getBoolean(BLINK_PREF, false);
}
public void setNotificationSound(String soundUri) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putString(SOUND_PREF, soundUri);
editor.commit();
}
public void setShouldVibrate(boolean vibrate) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putBoolean(VIBRATE_PREF, vibrate);
editor.commit();
}
public void setShouldBlink(boolean blink) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putBoolean(BLINK_PREF, blink);
editor.commit();
}
}

View File

@@ -1,106 +0,0 @@
package com.mattermost.rnbeta;
import android.app.Application;
import android.content.Context;
import android.database.Cursor;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.os.Bundle;
import android.net.Uri;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
private static NotificationPreferencesModule instance;
private final MainApplication mApplication;
private NotificationPreferences mNotificationPreference;
private NotificationPreferencesModule(MainApplication application, ReactApplicationContext reactContext) {
super(reactContext);
mApplication = application;
Context context = mApplication.getApplicationContext();
mNotificationPreference = NotificationPreferences.getInstance(context);
}
public static NotificationPreferencesModule getInstance(MainApplication application, ReactApplicationContext reactContext) {
if (instance == null) {
instance = new NotificationPreferencesModule(application, reactContext);
}
return instance;
}
public static NotificationPreferencesModule getInstance() {
return instance;
}
@Override
public String getName() {
return "NotificationPreferences";
}
@ReactMethod
public void getPreferences(final Promise promise) {
try {
Context context = mApplication.getApplicationContext();
RingtoneManager manager = new RingtoneManager(context);
manager.setType(RingtoneManager.TYPE_NOTIFICATION);
Cursor cursor = manager.getCursor();
WritableMap result = Arguments.createMap();
WritableArray sounds = Arguments.createArray();
while (cursor.moveToNext()) {
String notificationTitle = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX);
String notificationId = cursor.getString(RingtoneManager.ID_COLUMN_INDEX);
String notificationUri = cursor.getString(RingtoneManager.URI_COLUMN_INDEX);
WritableMap map = Arguments.createMap();
map.putString("name", notificationTitle);
map.putString("uri", (notificationUri + "/" + notificationId));
sounds.pushMap(map);
}
Uri defaultUri = RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_NOTIFICATION);
result.putString("defaultUri", Uri.decode(defaultUri.toString()));
result.putString("selectedUri", mNotificationPreference.getNotificationSound());
result.putBoolean("shouldVibrate", mNotificationPreference.getShouldVibrate());
result.putBoolean("shouldBlink", mNotificationPreference.getShouldBlink());
result.putArray("sounds", sounds);
promise.resolve(result);
} catch (Exception e) {
promise.reject("no notification sounds found", e);
}
}
@ReactMethod
public void previewSound(String url) {
Context context = mApplication.getApplicationContext();
Uri uri = Uri.parse(url);
Ringtone r = RingtoneManager.getRingtone(context, uri);
r.play();
}
@ReactMethod
public void setNotificationSound(String soundUri) {
mNotificationPreference.setNotificationSound(soundUri);
}
@ReactMethod
public void setShouldVibrate(boolean vibrate) {
mNotificationPreference.setShouldVibrate(vibrate);
}
@ReactMethod
public void setShouldBlink(boolean blink) {
mNotificationPreference.setShouldBlink(blink);
}
}

View File

@@ -1,49 +0,0 @@
package com.mattermost.rnbeta;
import android.content.Context;
import android.content.Intent;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.facebook.react.HeadlessJsTaskService;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationReplyService extends HeadlessJsTaskService {
@Override
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
if (CustomPushNotification.KEY_TEXT_REPLY.equals(intent.getAction())) {
CharSequence message = getReplyMessage(intent);
Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
String channelId = bundle.getString("channel_id");
bundle.putCharSequence("text", message);
bundle.putInt("msg_count", CustomPushNotification.getMessageCountInChannel(channelId));
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
CustomPushNotification.clearNotification(notificationId, channelId);
return new HeadlessJsTaskConfig(
"notificationReplied",
Arguments.fromBundle(bundle),
5000);
}
return null;
}
private CharSequence getReplyMessage(Intent intent) {
Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
return remoteInput.getCharSequence(CustomPushNotification.KEY_TEXT_REPLY);
}
return null;
}
}

View File

@@ -1,22 +1,9 @@
package com.mattermost.rnbeta;
import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.RestrictionsManager;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;
import android.util.ArraySet;
import android.view.WindowManager.LayoutParams;
import android.content.res.Configuration;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.reactnativenavigation.NavigationApplication;
import com.reactnativenavigation.controllers.ActivityCallbacks;
import com.reactnativenavigation.react.ReactGateway;
@@ -25,72 +12,16 @@ import com.wix.reactnativenotifications.core.AppLifecycleFacade;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
public class NotificationsLifecycleFacade extends ActivityCallbacks implements AppLifecycleFacade {
private static final String TAG = NotificationsLifecycleFacade.class.getSimpleName();
private static NotificationsLifecycleFacade instance;
private Bundle managedConfig = null;
private static final String TAG = NotificationsLifecycleFacade.class.getSimpleName();
private Activity mVisibleActivity;
private Set<AppVisibilityListener> mListeners = new CopyOnWriteArraySet<>();
private final IntentFilter restrictionsFilter =
new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
private final BroadcastReceiver restrictionsReceiver = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
if (mVisibleActivity != null) {
// Get the current configuration bundle
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) mVisibleActivity
.getSystemService(Context.RESTRICTIONS_SERVICE);
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
// Check current configuration settings, change your app's UI and
// functionality as necessary.
Log.i("ReactNative", "Managed Configuration Changed");
sendConfigChanged(managedConfig);
}
}
};
public static NotificationsLifecycleFacade getInstance() {
if (instance == null) {
instance = new NotificationsLifecycleFacade();
}
return instance;
}
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
MattermostManagedModule managedModule = MattermostManagedModule.getInstance();
if (managedModule != null && managedModule.isBlurAppScreenEnabled()) {
activity.getWindow().setFlags(LayoutParams.FLAG_SECURE,
LayoutParams.FLAG_SECURE);
}
if (managedConfig!= null && managedConfig.size() > 0) {
activity.registerReceiver(restrictionsReceiver, restrictionsFilter);
}
}
@Override
public void onActivityResumed(Activity activity) {
switchToVisible(activity);
if (managedConfig != null && managedConfig.size() > 0) {
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) activity
.getSystemService(Context.RESTRICTIONS_SERVICE);
Bundle newConfig = myRestrictionsMgr.getApplicationRestrictions();
if (!equalBundles(newConfig ,managedConfig)) {
Log.i("ReactNative", "onResumed Managed Configuration Changed");
managedConfig = newConfig;
sendConfigChanged(managedConfig);
}
}
}
@Override
@@ -101,13 +32,6 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
@Override
public void onActivityStopped(Activity activity) {
switchToInvisible(activity);
if (managedConfig != null && managedConfig.size() > 0) {
try {
activity.unregisterReceiver(restrictionsReceiver);
} catch (IllegalArgumentException e) {
// Just ignore this cause the receiver wasn't registered for this activity
}
}
}
@Override
@@ -145,15 +69,6 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
mListeners.remove(listener);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (mVisibleActivity != null) {
Intent intent = new Intent("onConfigurationChanged");
intent.putExtra("newConfig", newConfig);
mVisibleActivity.sendBroadcast(intent);
}
}
private synchronized void switchToVisible(Activity activity) {
if (mVisibleActivity == null) {
mVisibleActivity = activity;
@@ -173,67 +88,4 @@ public class NotificationsLifecycleFacade extends ActivityCallbacks implements A
}
}
}
public synchronized void LoadManagedConfig(Activity activity) {
RestrictionsManager myRestrictionsMgr =
(RestrictionsManager) activity
.getSystemService(Context.RESTRICTIONS_SERVICE);
managedConfig = myRestrictionsMgr.getApplicationRestrictions();
myRestrictionsMgr = null;
}
public synchronized Bundle getManagedConfig() {
if (managedConfig!= null && managedConfig.size() > 0) {
return managedConfig;
}
if (mVisibleActivity != null) {
LoadManagedConfig(mVisibleActivity);
return managedConfig;
}
return null;
}
public void sendConfigChanged(Bundle config) {
Object result = Arguments.fromBundle(config);
getRunningReactContext().
getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).
emit("managedConfigDidChange", result);
}
private boolean equalBundles(Bundle one, Bundle two) {
if (one == null || two == null)
return false;
if(one.size() != two.size())
return false;
Set<String> setOne = new ArraySet<String>();
setOne.addAll(one.keySet());
setOne.addAll(two.keySet());
Object valueOne;
Object valueTwo;
for(String key : setOne) {
if (!one.containsKey(key) || !two.containsKey(key))
return false;
valueOne = one.get(key);
valueTwo = two.get(key);
if(valueOne instanceof Bundle && valueTwo instanceof Bundle &&
!equalBundles((Bundle) valueOne, (Bundle) valueTwo)) {
return false;
}
else if(valueOne == null) {
if(valueTwo != null)
return false;
}
else if(!valueOne.equals(valueTwo))
return false;
}
return true;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,20 +1,6 @@
<?xml version="1.0"?>
<resources>
<string name="app_name">Mattermost Beta</string>
<string name="inAppPinCode_title">in-App Pincode</string>
<string name="inAppPinCode_description">Require users to authenticate as the owner of the phone before using the app. Prompts for fingerprint or passcode when the app first opens and when the app has been in the background for more than 5 minutes.</string>
<string name="blurApplicationScreen_title">Blur Application Screen</string>
<string name="blurApplicationScreen_description">Blur the app when its set to background to protect any confidential on-screen information, it also prevents taking screenshots of the app.</string>
<string name="jailbreakProtection_title">Jailbreak &#x2F; Root Detection</string>
<string name="jailbreakProtection_description">Disable app launch on Jailbroken or rooted devices.</string>
<string name="copyAndPasteProtection_title">Copy&amp;Paste Protection</string>
<string name="copyAndPasteProtection_description">Disable the ability to copy from or paste into any text inputs in the app.</string>
<string name="serverUrl_title">Mattermost Server URL</string>
<string name="serverUrl_description">Set a default Mattermost server URL.</string>
<string name="allowOtherServers_title">Allow Other Servers</string>
<string name="allowOtherServers_description">Allow the user to change the above server URL.</string>
<string name="username_title">Default Username</string>
<string name="username_description">Set the username or email address to use to authenticate against the Mattermost Server.</string>
<string name="vendor_title">EMM Vendor or Company Name</string>
<string name="vendor_description">Name of the EMM vendor or company deploying the app. Used in help text when prompting for passcodes so users are aware why the app is being protected.</string>
</resources>

View File

@@ -1,53 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<restrictions xmlns:android="http://schemas.android.com/apk/res/android">
<restriction
android:key="inAppPinCode"
android:title="@string/inAppPinCode_title"
android:description="@string/inAppPinCode_description"
android:restrictionType="string"
android:defaultValue="false" />
<restriction
android:key="blurApplicationScreen"
android:title="@string/blurApplicationScreen_title"
android:description="@string/blurApplicationScreen_description"
android:restrictionType="string"
android:defaultValue="false" />
<restriction
android:key="jailbreakProtection"
android:title="@string/jailbreakProtection_title"
android:description="@string/jailbreakProtection_description"
android:restrictionType="string"
android:defaultValue="false" />
<restriction
android:key="copyAndPasteProtection"
android:title="@string/copyAndPasteProtection_title"
android:description="@string/copyAndPasteProtection_description"
android:restrictionType="string"
android:defaultValue="false" />
<restriction
android:key="serverUrl"
android:title="@string/serverUrl_title"
android:description="@string/serverUrl_description"
android:restrictionType="string"
android:defaultValue="" />
<restriction
android:key="allowOtherServers"
android:title="@string/allowOtherServers_title"
android:description="@string/allowOtherServers_description"
android:restrictionType="string"
android:defaultValue="true" />
<restriction
android:key="username"
android:title="@string/username_title"
android:description="@string/username_description"
android:restrictionType="string"
android:defaultValue="" />
<restriction
android:key="vendor"
android:title="@string/vendor_title"
android:description="@string/vendor_description"
android:restrictionType="string"
android:defaultValue="" />
</restrictions>

View File

@@ -1,16 +1,4 @@
rootProject.name = 'Mattermost'
include ':react-native-youtube'
project(':react-native-youtube').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-youtube/android')
include ':react-native-sentry'
project(':react-native-sentry').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sentry/android')
include ':react-native-exception-handler'
project(':react-native-exception-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-exception-handler/android')
include ':react-native-fetch-blob'
project(':react-native-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fetch-blob/android')
include ':jail-monkey'
project(':jail-monkey').projectDir = new File(rootProject.projectDir, '../node_modules/jail-monkey/android')
include ':react-native-local-auth'
project(':react-native-local-auth').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-local-auth/android')
include ':react-native-navigation'
project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/android/app/')
include ':react-native-image-picker'

View File

@@ -1,55 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {Dimensions} from 'react-native';
import {DeviceTypes} from 'app/constants';
export function calculateDeviceDimensions() {
const {height, width} = Dimensions.get('window');
return {
type: DeviceTypes.DEVICE_DIMENSIONS_CHANGED,
data: {
deviceHeight: height,
deviceWidth: width
}
};
}
export function connection(isOnline) {
return async (dispatch, getState) => {
dispatch({
type: DeviceTypes.CONNECTION_CHANGED,
data: isOnline
}, getState);
};
}
export function setStatusBarHeight(height = 20) {
return {
type: DeviceTypes.STATUSBAR_HEIGHT_CHANGED,
data: height
};
}
export function setDeviceOrientation(orientation) {
return {
type: DeviceTypes.DEVICE_ORIENTATION_CHANGED,
data: orientation
};
}
export function setDeviceAsTablet() {
return {
type: DeviceTypes.DEVICE_TYPE_CHANGED,
data: true
};
}
export default {
calculateDeviceDimensions,
connection,
setDeviceOrientation,
setDeviceAsTablet,
setStatusBarHeight
};

View File

@@ -11,12 +11,12 @@ export function handleUpdateUserNotifyProps(notifyProps) {
const config = state.entities.general.config;
const {currentUserId} = state.entities.users;
const {interval, user_id, ...otherProps} = notifyProps;
const {interval, ...otherProps} = notifyProps;
const email = notifyProps.email;
if (config.EnableEmailBatching === 'true' && email !== 'false') {
const emailInterval = [{
user_id,
user_id: notifyProps.user_id,
category: Preferences.CATEGORY_NOTIFICATIONS,
name: Preferences.EMAIL_INTERVAL,
value: interval

View File

@@ -10,12 +10,11 @@ import {
fetchMyChannelsAndMembers,
getChannelStats,
selectChannel,
leaveChannel as serviceLeaveChannel,
unfavoriteChannel
leaveChannel as serviceLeaveChannel
} from 'mattermost-redux/actions/channels';
import {getPosts, getPostsBefore, getPostsSince, getPostThread} from 'mattermost-redux/actions/posts';
import {getPosts, getPostsWithRetry, getPostsBefore, getPostsSinceWithRetry, getPostThread} from 'mattermost-redux/actions/posts';
import {getFilesForPost} from 'mattermost-redux/actions/files';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {savePreferences, deletePreferences} from 'mattermost-redux/actions/preferences';
import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
import {General, Preferences} from 'mattermost-redux/constants';
@@ -31,8 +30,6 @@ import {
import {getLastCreateAt} from 'mattermost-redux/utils/post_utils';
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
const MAX_POST_TRIES = 3;
export function loadChannelsIfNecessary(teamId) {
return async (dispatch, getState) => {
await fetchMyChannelsAndMembers(teamId)(dispatch, getState);
@@ -42,7 +39,7 @@ export function loadChannelsIfNecessary(teamId) {
export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
return async (dispatch, getState) => {
const state = getState();
const {currentUserId, profilesInChannel} = state.entities.users;
const {currentUserId} = state.entities.users;
const {channels, myMembers} = state.entities.channels;
const {myPreferences} = state.entities.preferences;
const {membersInTeam} = state.entities.teams;
@@ -99,8 +96,7 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
}
for (const [key, pref] of gmPrefs) {
//only load the profiles in channels if we don't already have them
if (pref.value === 'true' && !profilesInChannel[key]) {
if (pref.value === 'true') {
loadProfilesForChannels.push(key);
}
}
@@ -141,57 +137,25 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
}
export function loadPostsIfNecessaryWithRetry(channelId) {
return async (dispatch, getState) => {
return (dispatch, getState) => {
const state = getState();
const {posts, postsInChannel} = state.entities.posts;
const postsIds = postsInChannel[channelId];
const time = Date.now();
let received;
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
if (!postsIds || postsIds.length < ViewTypes.POST_VISIBILITY_CHUNK_SIZE) {
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
received = await retryGetPostsAction(getPosts(channelId), dispatch, getState);
} else {
const {lastConnectAt} = state.device.websocket;
const lastGetPosts = state.views.channel.lastGetPosts[channelId];
let since;
if (lastGetPosts && lastGetPosts < lastConnectAt) {
// Since the websocket disconnected, we may have missed some posts since then
since = lastGetPosts;
} else {
// Trust that we've received all posts since the last time the websocket disconnected
// so just get any that have changed since the latest one we've received
const postsForChannel = postsIds.map((id) => posts[id]);
since = getLastCreateAt(postsForChannel);
}
received = await retryGetPostsAction(getPostsSince(channelId, since), dispatch, getState);
getPostsWithRetry(channelId)(dispatch, getState);
return;
}
if (received) {
dispatch({
type: ViewTypes.RECEIVED_POSTS_FOR_CHANNEL_AT_TIME,
channelId,
time
});
}
const postsForChannel = postsIds.map((id) => posts[id]);
const latestPostTime = getLastCreateAt(postsForChannel);
getPostsSinceWithRetry(channelId, latestPostTime)(dispatch, getState);
};
}
async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_POST_TRIES) {
for (let i = 0; i < maxTries; i++) {
const posts = await action(dispatch, getState);
if (posts) {
return posts;
}
}
return null;
}
export function loadFilesForPostIfNecessary(postId) {
return async (dispatch, getState) => {
const {files} = getState().entities;
@@ -203,14 +167,13 @@ export function loadFilesForPostIfNecessary(postId) {
};
}
export function loadThreadIfNecessary(rootId, channelId) {
export function loadThreadIfNecessary(rootId) {
return async (dispatch, getState) => {
const state = getState();
const {posts, postsInChannel} = state.entities.posts;
const channelPosts = postsInChannel[channelId];
const {posts} = state.entities.posts;
if (rootId && (!posts[rootId] || !channelPosts || !channelPosts[rootId])) {
getPostThread(rootId, false)(dispatch, getState);
if (rootId && !posts[rootId]) {
getPostThread(rootId)(dispatch, getState);
}
};
}
@@ -218,35 +181,34 @@ export function loadThreadIfNecessary(rootId, channelId) {
export function selectInitialChannel(teamId) {
return async (dispatch, getState) => {
const state = getState();
const {channels, myMembers} = state.entities.channels;
const {channels, currentChannelId, myMembers} = state.entities.channels;
const {currentUserId} = state.entities.users;
const currentChannel = channels[currentChannelId];
const {myPreferences} = state.entities.preferences;
const lastChannelId = state.views.team.lastChannelForTeam[teamId] || '';
const lastChannel = channels[lastChannelId];
const isDMVisible = lastChannel && lastChannel.type === General.DM_CHANNEL &&
isDirectChannelVisible(currentUserId, myPreferences, lastChannel);
const isDMVisible = currentChannel && currentChannel.type === General.DM_CHANNEL &&
isDirectChannelVisible(currentUserId, myPreferences, currentChannel);
const isGMVisible = lastChannel && lastChannel.type === General.GM_CHANNEL &&
isGroupChannelVisible(myPreferences, lastChannel);
const isGMVisible = currentChannel && currentChannel.type === General.GM_CHANNEL &&
isGroupChannelVisible(myPreferences, currentChannel);
if (lastChannelId && myMembers[lastChannelId] &&
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)) {
handleSelectChannel(lastChannelId)(dispatch, getState);
if (currentChannel && myMembers[currentChannelId] &&
(currentChannel.team_id === teamId || isDMVisible || isGMVisible)) {
await handleSelectChannel(currentChannelId)(dispatch, getState);
return;
}
const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL);
if (channel) {
dispatch(setChannelDisplayName(''));
handleSelectChannel(channel.id)(dispatch, getState);
await handleSelectChannel(channel.id)(dispatch, getState);
} else {
// Handle case when the default channel cannot be found
// so we need to get the first available channel of the team
const channelsInTeam = Object.values(channels).filter((c) => c.team_id === teamId);
const firstChannel = channelsInTeam.length ? channelsInTeam[0].id : {id: ''};
dispatch(setChannelDisplayName(''));
handleSelectChannel(firstChannel.id)(dispatch, getState);
await handleSelectChannel(firstChannel.id)(dispatch, getState);
}
};
}
@@ -255,15 +217,13 @@ export function handleSelectChannel(channelId) {
return async (dispatch, getState) => {
const {currentTeamId} = getState().entities.teams;
selectChannel(channelId)(dispatch, getState);
dispatch(setChannelLoading(false));
dispatch({
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
teamId: currentTeamId,
channelId
});
getChannelStats(channelId)(dispatch, getState);
selectChannel(channelId)(dispatch, getState);
};
}
@@ -314,7 +274,7 @@ export function closeDMChannel(channel) {
const state = getState();
if (channel.isFavorite) {
unfavoriteChannel(channel.id)(dispatch, getState);
unmarkFavorite(channel.id)(dispatch, getState);
}
toggleDMChannel(channel.teammate_id, 'false')(dispatch, getState);
@@ -329,7 +289,7 @@ export function closeGMChannel(channel) {
const state = getState();
if (channel.isFavorite) {
unfavoriteChannel(channel.id)(dispatch, getState);
unmarkFavorite(channel.id)(dispatch, getState);
}
toggleGMChannel(channel.id, 'false')(dispatch, getState);
@@ -339,9 +299,36 @@ export function closeGMChannel(channel) {
};
}
export function markFavorite(channelId) {
return async (dispatch, getState) => {
const {currentUserId} = getState().entities.users;
const fav = [{
user_id: currentUserId,
category: Preferences.CATEGORY_FAVORITE_CHANNEL,
name: channelId,
value: 'true'
}];
savePreferences(currentUserId, fav)(dispatch, getState);
};
}
export function unmarkFavorite(channelId) {
return async (dispatch, getState) => {
const {currentUserId} = getState().entities.users;
const fav = [{
user_id: currentUserId,
category: Preferences.CATEGORY_FAVORITE_CHANNEL,
name: channelId
}];
deletePreferences(currentUserId, fav)(dispatch, getState);
};
}
export function refreshChannelWithRetry(channelId) {
return (dispatch, getState) => {
return retryGetPostsAction(getPosts(channelId), dispatch, getState);
getPostsWithRetry(channelId)(dispatch, getState);
};
}
@@ -362,13 +349,6 @@ export function setChannelLoading(loading = true) {
};
}
export function setChannelRefreshing(loading = true) {
return {
type: ViewTypes.SET_CHANNEL_REFRESHING,
loading
};
}
export function setPostTooltipVisible(visible = true) {
return {
type: ViewTypes.POST_TOOLTIP_VISIBLE,

View File

@@ -0,0 +1,13 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';
export function connection(isOnline) {
return async (dispatch, getState) => {
dispatch({
type: ViewTypes.CONNECTION_CHANGED,
data: isOnline
}, getState);
};
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {addReaction} from 'mattermost-redux/actions/posts';
import {getPostsInCurrentChannel, makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts';
const getPostsForThread = makeGetPostsForThread();
export function addReactionToLatestPost(emoji, rootId) {
return async (dispatch, getState) => {
const state = getState();
const posts = rootId ? getPostsForThread(state, {rootId}) : getPostsInCurrentChannel(state);
const lastPost = posts[0];
dispatch(addReaction(lastPost.id, emoji));
};
}

View File

@@ -4,22 +4,21 @@
import {ViewTypes} from 'app/constants';
import {
handleSelectChannel,
loadChannelsIfNecessary,
loadProfilesAndTeamMembersForDMSidebar,
setChannelDisplayName
} from 'app/actions/views/channel';
import {handleTeamChange, selectFirstAvailableTeam} from 'app/actions/views/select_team';
import {General} from 'mattermost-redux/constants';
import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general';
import {getChannelAndMyMember, markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
import {getClientConfig, getLicenseConfig, setServerVersion} from 'mattermost-redux/actions/general';
import {markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
export function loadConfigAndLicense() {
export function loadConfigAndLicense(serverVersion) {
return async (dispatch, getState) => {
const [config, license] = await Promise.all([
getClientConfig()(dispatch, getState),
getLicenseConfig()(dispatch, getState)
]);
return {config, license};
getClientConfig()(dispatch, getState);
getLicenseConfig()(dispatch, getState);
setServerVersion(serverVersion)(dispatch, getState);
};
}
@@ -40,34 +39,39 @@ export function goToNotification(notification) {
const state = getState();
const {data} = notification;
const {currentTeamId, teams} = state.entities.teams;
const {channels, currentChannelId, myMembers} = state.entities.channels;
const {currentChannelId} = state.entities.channels;
const channelId = data.channel_id;
// if the notification does not have a team id is because its from a DM or GM
const teamId = data.team_id || currentTeamId;
let teamId = data.team_id || currentTeamId;
dispatch(setChannelDisplayName(''));
if (teamId && teamId !== currentTeamId) {
if (teamId) {
handleTeamChange(teams[teamId], false)(dispatch, getState);
} else if (!teamId) {
await loadChannelsIfNecessary(teamId)(dispatch, getState);
} else {
await selectFirstAvailableTeam()(dispatch, getState);
teamId = state.entities.team.currentTeamId;
}
if (!channels[channelId] || !myMembers[channelId]) {
getChannelAndMyMember(channelId)(dispatch, getState);
}
viewChannel(channelId)(dispatch, getState);
loadProfilesAndTeamMembersForDMSidebar(teamId)(dispatch, getState);
if (channelId !== currentChannelId) {
handleSelectChannel(channelId)(dispatch, getState);
}
viewChannel(channelId)(dispatch, getState);
markChannelAsRead(channelId, currentChannelId)(dispatch, getState);
};
}
export function setStatusBarHeight(height = 20) {
return {
type: ViewTypes.STATUSBAR_HEIGHT_CHANGED,
data: height
};
}
export function purgeOfflineStore() {
return {type: General.OFFLINE_STORE_PURGE};
}
@@ -77,5 +81,5 @@ export default {
queueNotification,
clearNotification,
goToNotification,
purgeOfflineStore
setStatusBarHeight
};

View File

@@ -3,10 +3,8 @@
import {batchActions} from 'redux-batched-actions';
import {markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
import {ChannelTypes, TeamTypes} from 'mattermost-redux/action_types';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {NavigationTypes} from 'app/constants';
@@ -14,24 +12,20 @@ import {setChannelDisplayName} from './channel';
export function handleTeamChange(team, selectChannel = true) {
return async (dispatch, getState) => {
const state = getState();
const {currentTeamId} = state.entities.teams;
const {currentTeamId} = getState().entities.teams;
if (currentTeamId === team.id) {
return;
}
const state = getState();
const actions = [
setChannelDisplayName(''),
{type: TeamTypes.SELECT_TEAM, data: team.id}
];
if (selectChannel) {
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: ''});
const lastChannelId = state.views.team.lastChannelForTeam[team.id] || '';
const currentChannelId = getCurrentChannelId(state);
viewChannel(lastChannelId, currentChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId, currentChannelId)(dispatch, getState);
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: lastChannelId});
}
dispatch(batchActions(actions), getState);

View File

@@ -1,39 +1,48 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {SectionList} from 'react-native';
import {
ListView,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {sortByUsername} from 'mattermost-redux/utils/user_utils';
import FormattedText from 'app/components/formatted_text';
import ProfilePicture from 'app/components/profile_picture';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import {RequestStatus} from 'mattermost-redux/constants';
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
import AtMentionItem from 'app/components/autocomplete/at_mention_item';
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
import SpecialMentionItem from 'app/components/autocomplete/special_mention_item';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
const AT_MENTION_REGEX = /\B(@([^@\r\n\s]*))$/i;
const FROM_REGEX = /\bfrom:\s*(\S*)$/i;
export default class AtMention extends PureComponent {
export default class AtMention extends Component {
static propTypes = {
actions: PropTypes.shape({
autocompleteUsers: PropTypes.func.isRequired
}).isRequired,
currentChannelId: PropTypes.string,
currentUserId: PropTypes.string.isRequired,
currentChannelId: PropTypes.string.isRequired,
currentTeamId: PropTypes.string.isRequired,
cursorPosition: PropTypes.number.isRequired,
defaultChannel: PropTypes.object,
inChannel: PropTypes.array,
defaultChannel: PropTypes.object.isRequired,
autocompleteUsers: PropTypes.object.isRequired,
isSearch: PropTypes.bool,
matchTerm: PropTypes.string,
onChangeText: PropTypes.func.isRequired,
outChannel: PropTypes.array,
postDraft: PropTypes.string,
requestStatus: PropTypes.string.isRequired,
teamMembers: PropTypes.array,
theme: PropTypes.object.isRequired
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
actions: PropTypes.shape({
autocompleteUsers: PropTypes.func.isRequired
})
};
static defaultProps = {
autocompleteUsers: {},
defaultChannel: {},
postDraft: '',
isSearch: false
@@ -42,82 +51,74 @@ export default class AtMention extends PureComponent {
constructor(props) {
super(props);
const ds = new ListView.DataSource({
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
rowHasChanged: (r1, r2) => r1 !== r2
});
const data = {};
this.state = {
sections: []
active: false,
dataSource: ds.cloneWithRowsAndSections(data)
};
}
componentWillReceiveProps(nextProps) {
const {inChannel, outChannel, teamMembers, isSearch, matchTerm, requestStatus} = nextProps;
if ((matchTerm !== this.props.matchTerm && matchTerm === null) || this.state.mentionComplete) {
// if the term changes but is null or the mention has been completed we render this component as null
const {isSearch} = nextProps;
const regex = isSearch ? FROM_REGEX : AT_MENTION_REGEX;
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex);
if (!match || this.state.mentionComplete) {
this.setState({
mentionComplete: false,
sections: []
active: false,
matchTerm: null,
mentionComplete: false
});
return;
} else if (matchTerm === null) {
// if the terms did not change but is null then we don't need to do anything
return;
}
if (matchTerm !== this.props.matchTerm) {
// if the term changed and we haven't made the request do that first
const matchTerm = isSearch ? match[1] : match[2];
if (matchTerm !== this.state.matchTerm) {
this.setState({
matchTerm
});
const {currentTeamId, currentChannelId} = this.props;
const channelId = isSearch ? '' : currentChannelId;
this.props.actions.autocompleteUsers(matchTerm, currentTeamId, channelId);
return;
this.props.actions.autocompleteUsers(matchTerm, currentTeamId, currentChannelId);
}
if (requestStatus !== RequestStatus.STARTED &&
(inChannel !== this.props.inChannel || outChannel !== this.props.outChannel || teamMembers !== this.props.teamMembers)) {
// if the request is complete and the term is not null we show the autocomplete
const sections = [];
if (nextProps.requestStatus !== RequestStatus.STARTED) {
const membersInChannel = this.filter(nextProps.autocompleteUsers.inChannel, matchTerm) || [];
const membersOutOfChannel = this.filter(nextProps.autocompleteUsers.outChannel, matchTerm) || [];
let data = {};
if (isSearch) {
sections.push({
id: 'mobile.suggestion.members',
defaultMessage: 'Members',
data: teamMembers,
key: 'teamMembers'
});
data = {members: membersInChannel.concat(membersOutOfChannel).sort(sortByUsername)};
} else {
if (inChannel.length) {
sections.push({
id: 'suggestion.mention.members',
defaultMessage: 'Channel Members',
data: inChannel,
key: 'inChannel'
});
if (membersInChannel.length > 0) {
data = Object.assign({}, data, {inChannel: membersInChannel});
}
if (this.checkSpecialMentions(matchTerm)) {
sections.push({
id: 'suggestion.mention.special',
defaultMessage: 'Special Mentions',
data: this.getSpecialMentions(),
key: 'special',
renderItem: this.renderSpecialMentions
});
if (this.checkSpecialMentions(matchTerm) && !isSearch) {
data = Object.assign({}, data, {specialMentions: this.getSpecialMentions()});
}
if (outChannel.length) {
sections.push({
id: 'suggestion.mention.nonmembers',
defaultMessage: 'Not in Channel',
data: outChannel,
key: 'outChannel'
});
if (membersOutOfChannel.length > 0) {
data = Object.assign({}, data, {notInChannel: membersOutOfChannel});
}
}
this.setState({
sections
active: data.hasOwnProperty('inChannel') || data.hasOwnProperty('specialMentions') || data.hasOwnProperty('notInChannel') || data.hasOwnProperty('members'),
dataSource: this.state.dataSource.cloneWithRowsAndSections(data)
});
}
}
keyExtractor = (item) => {
return item.id || item;
filter = (profiles, matchTerm) => {
const {isSearch} = this.props;
return profiles.filter((p) => {
return ((p.id !== this.props.currentUserId || isSearch) && (
p.username.toLowerCase().includes(matchTerm) || p.email.toLowerCase().includes(matchTerm) ||
p.first_name.toLowerCase().includes(matchTerm) || p.last_name.toLowerCase().includes(matchTerm)));
});
};
getSpecialMentions = () => {
@@ -149,7 +150,7 @@ export default class AtMention extends PureComponent {
let completedDraft;
if (isSearch) {
completedDraft = mentionPart.replace(AT_MENTION_SEARCH_REGEX, `from: ${mention} `);
completedDraft = mentionPart.replace(FROM_REGEX, `from: ${mention} `);
} else {
completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
}
@@ -158,75 +159,214 @@ export default class AtMention extends PureComponent {
completedDraft += postDraft.substring(cursorPosition);
}
onChangeText(completedDraft, true);
this.setState({mentionComplete: true});
onChangeText(completedDraft);
this.setState({
active: false,
mentionComplete: true
});
};
renderSectionHeader = ({section}) => {
renderSectionHeader = (sectionData, sectionId) => {
const style = getStyleFromTheme(this.props.theme);
const localization = {
inChannel: {
id: 'suggestion.mention.members',
defaultMessage: 'Channel Members'
},
notInChannel: {
id: 'suggestion.mention.nonmembers',
defaultMessage: 'Not in Channel'
},
specialMentions: {
id: 'suggestion.mention.special',
defaultMessage: 'Special Mentions'
},
members: {
id: 'mobile.suggestion.members',
defaultMessage: 'Members'
}
};
return (
<AutocompleteSectionHeader
id={section.id}
defaultMessage={section.defaultMessage}
theme={this.props.theme}
/>
<View style={style.sectionWrapper}>
<View style={style.section}>
<FormattedText
id={localization[sectionId].id}
defaultMessage={localization[sectionId].defaultMessage}
style={style.sectionText}
/>
</View>
</View>
);
};
renderItem = ({item}) => {
renderRow = (data, sectionId) => {
if (sectionId === 'specialMentions') {
return this.renderSpecialMentions(data);
}
const style = getStyleFromTheme(this.props.theme);
const hasFullName = data.first_name.length > 0 && data.last_name.length > 0;
return (
<AtMentionItem
onPress={this.completeMention}
userId={item}
/>
<TouchableOpacity
onPress={() => this.completeMention(data.username)}
style={style.row}
>
<View style={style.rowPicture}>
<ProfilePicture
user={data}
theme={this.props.theme}
size={20}
status={null}
/>
</View>
<Text style={style.rowUsername}>{`@${data.username}`}</Text>
{hasFullName && <Text style={style.rowUsername}>{' - '}</Text>}
{hasFullName && <Text style={style.rowFullname}>{`${data.first_name} ${data.last_name}`}</Text>}
</TouchableOpacity>
);
};
renderSpecialMentions = ({item}) => {
renderSpecialMentions = (data) => {
const style = getStyleFromTheme(this.props.theme);
return (
<SpecialMentionItem
completeHandle={item.completeHandle}
defaultMessage={item.defaultMessage}
id={item.id}
onPress={this.completeMention}
theme={this.props.theme}
values={item.values}
/>
<TouchableOpacity
onPress={() => this.completeMention(data.completeHandle)}
style={style.row}
>
<View style={style.rowPicture}>
<Icon
name='users'
style={style.rowIcon}
/>
</View>
<Text style={style.textWrapper}>
<Text style={style.rowUsername}>{`@${data.completeHandle}`}</Text>
<Text style={style.rowUsername}>{' - '}</Text>
<FormattedText
id={data.id}
defaultMessage={data.defaultMessage}
values={data.values}
style={[style.rowFullname, {flex: 1}]}
/>
</Text>
</TouchableOpacity>
);
};
render() {
const {isSearch, theme} = this.props;
const {mentionComplete, sections} = this.state;
if (sections.length === 0 || mentionComplete) {
// If we are not in an active state or the mention has been completed return null so nothing is rendered
const {autocompleteUsers, requestStatus} = this.props;
if (!this.state.active && (requestStatus !== RequestStatus.STARTED || requestStatus !== RequestStatus.SUCCESS)) {
// If we are not in an active state return null so nothing is rendered
// other components are not blocked.
return null;
}
const style = getStyleFromTheme(theme);
const style = getStyleFromTheme(this.props.theme);
if (
!autocompleteUsers.inChannel &&
!autocompleteUsers.outChannel &&
requestStatus === RequestStatus.STARTED
) {
return (
<View style={style.loading}>
<FormattedText
id='analytics.chart.loading'
defaultMessage='Loading...'
style={style.sectionText}
/>
</View>
);
}
return (
<SectionList
<ListView
keyboardShouldPersistTaps='always'
keyExtractor={this.keyExtractor}
style={[style.listView, isSearch ? style.search : null]}
sections={sections}
renderItem={this.renderItem}
style={style.listView}
enableEmptySections={true}
dataSource={this.state.dataSource}
renderSectionHeader={this.renderSectionHeader}
initialNumToRender={10}
renderRow={this.renderRow}
renderFooter={this.renderFooter}
pageSize={10}
initialListSize={10}
/>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
listView: {
return StyleSheet.create({
section: {
justifyContent: 'center',
paddingLeft: 8,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
sectionText: {
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.7),
paddingVertical: 7
},
sectionWrapper: {
backgroundColor: theme.centerChannelBg
},
search: {
height: 250
listView: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
loading: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 20,
backgroundColor: theme.centerChannelBg,
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderBottomWidth: 0
},
row: {
paddingVertical: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowIcon: {
color: changeOpacity(theme.centerChannelColor, 0.7),
fontSize: 14
},
rowPicture: {
marginHorizontal: 8,
width: 20,
alignItems: 'center',
justifyContent: 'center'
},
rowUsername: {
fontSize: 13,
color: theme.centerChannelColor
},
rowFullname: {
color: theme.centerChannelColor,
opacity: 0.6
},
textWrapper: {
flex: 1,
flexWrap: 'wrap',
paddingRight: 8
}
};
});
});

View File

@@ -4,29 +4,21 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {autocompleteUsers} from 'mattermost-redux/actions/users';
import {getCurrentChannelId, getDefaultChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {
filterMembersInChannel,
filterMembersNotInChannel,
filterMembersInCurrentTeam,
getMatchTermForAtMention
} from 'app/selectors/autocomplete';
import {getTheme} from 'app/selectors/preferences';
import {autocompleteUsers} from 'mattermost-redux/actions/users';
import {getDefaultChannel} from 'mattermost-redux/selectors/entities/channels';
import {getProfilesInCurrentChannel, getProfilesNotInCurrentChannel} from 'mattermost-redux/selectors/entities/users';
import AtMention from './at_mention';
function mapStateToProps(state, ownProps) {
const {cursorPosition, isSearch, rootId} = ownProps;
const currentChannelId = getCurrentChannelId(state);
const {currentChannelId} = state.entities.channels;
let postDraft = '';
if (isSearch) {
let postDraft;
if (ownProps.isSearch) {
postDraft = state.views.search;
} else if (ownProps.rootId) {
const threadDraft = state.views.thread.drafts[rootId];
const threadDraft = state.views.thread.drafts[ownProps.rootId];
if (threadDraft) {
postDraft = threadDraft.draft;
}
@@ -37,28 +29,16 @@ function mapStateToProps(state, ownProps) {
}
}
const value = postDraft.substring(0, cursorPosition);
const matchTerm = getMatchTermForAtMention(value, isSearch);
let teamMembers;
let inChannel;
let outChannel;
if (isSearch) {
teamMembers = filterMembersInCurrentTeam(state, matchTerm);
} else {
inChannel = filterMembersInChannel(state, matchTerm);
outChannel = filterMembersNotInChannel(state, matchTerm);
}
return {
currentUserId: state.entities.users.currentUserId,
currentChannelId,
currentTeamId: getCurrentTeamId(state),
currentTeamId: state.entities.teams.currentTeamId,
defaultChannel: getDefaultChannel(state),
postDraft,
matchTerm,
teamMembers,
inChannel,
outChannel,
autocompleteUsers: {
inChannel: getProfilesInCurrentChannel(state),
outChannel: getProfilesNotInCurrentChannel(state)
},
requestStatus: state.requests.users.autocompleteUsers.status,
theme: getTheme(state),
...ownProps

View File

@@ -1,92 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity,
View
} from 'react-native';
import ProfilePicture from 'app/components/profile_picture';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class AtMentionItem extends PureComponent {
static propTypes = {
firstName: PropTypes.string,
lastName: PropTypes.string,
onPress: PropTypes.func.isRequired,
userId: PropTypes.string.isRequired,
username: PropTypes.string,
theme: PropTypes.object.isRequired
};
completeMention = () => {
const {onPress, username} = this.props;
onPress(username);
};
render() {
const {
firstName,
lastName,
userId,
username,
theme
} = this.props;
const style = getStyleFromTheme(theme);
const hasFullName = firstName.length > 0 && lastName.length > 0;
return (
<TouchableOpacity
key={userId}
onPress={this.completeMention}
style={style.row}
>
<View style={style.rowPicture}>
<ProfilePicture
userId={userId}
theme={theme}
size={20}
status={null}
/>
</View>
<Text style={style.rowUsername}>{`@${username}`}</Text>
{hasFullName && <Text style={style.rowUsername}>{' - '}</Text>}
{hasFullName && <Text style={style.rowFullname}>{`${firstName} ${lastName}`}</Text>}
</TouchableOpacity>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
paddingVertical: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowPicture: {
marginHorizontal: 8,
width: 20,
alignItems: 'center',
justifyContent: 'center'
},
rowUsername: {
fontSize: 13,
color: theme.centerChannelColor
},
rowFullname: {
color: theme.centerChannelColor,
opacity: 0.6
}
};
});

View File

@@ -1,24 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getUser} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'app/selectors/preferences';
import AtMentionItem from './at_mention_item';
function mapStateToProps(state, ownProps) {
const user = getUser(state, ownProps.userId);
return {
firstName: user.first_name,
lastName: user.last_name,
username: user.username,
theme: getTheme(state),
...ownProps
};
}
export default connect(mapStateToProps)(AtMentionItem);

View File

@@ -1,58 +0,0 @@
// Copyright (c) 2017-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 FormattedText from 'app/components/formatted_text';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class AutocompleteSectionHeader extends PureComponent {
static propTypes = {
defaultMessage: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired
};
render() {
const {defaultMessage, id, theme} = this.props;
const style = getStyleFromTheme(theme);
return (
<View style={style.sectionWrapper}>
<View style={style.section}>
<FormattedText
id={id}
defaultMessage={defaultMessage}
style={style.sectionText}
/>
</View>
</View>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
section: {
justifyContent: 'center',
paddingLeft: 8,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
sectionText: {
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.7),
paddingVertical: 7
},
sectionWrapper: {
backgroundColor: theme.centerChannelBg
}
};
});

View File

@@ -1,34 +1,38 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {SectionList} from 'react-native';
import {
ListView,
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
import {RequestStatus} from 'mattermost-redux/constants';
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
import ChannelMentionItem from 'app/components/autocomplete/channel_mention_item';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
const CHANNEL_MENTION_REGEX = /\B(~([^~\r\n]*))$/i;
const CHANNEL_SEARCH_REGEX = /\b(?:in|channel):\s*(\S*)$/i;
export default class ChannelMention extends PureComponent {
export default class ChannelMention extends Component {
static propTypes = {
actions: PropTypes.shape({
searchChannels: PropTypes.func.isRequired
}).isRequired,
currentChannelId: PropTypes.string.isRequired,
currentTeamId: PropTypes.string.isRequired,
cursorPosition: PropTypes.number.isRequired,
isSearch: PropTypes.bool,
matchTerm: PropTypes.string,
myChannels: PropTypes.array,
otherChannels: PropTypes.array,
onChangeText: PropTypes.func.isRequired,
autocompleteChannels: PropTypes.object.isRequired,
postDraft: PropTypes.string,
privateChannels: PropTypes.array,
publicChannels: PropTypes.array,
isSearch: PropTypes.bool,
requestStatus: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
actions: PropTypes.shape({
searchChannels: PropTypes.func.isRequired
})
};
static defaultProps = {
@@ -39,82 +43,97 @@ export default class ChannelMention extends PureComponent {
constructor(props) {
super(props);
const ds = new ListView.DataSource({
sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
rowHasChanged: (r1, r2) => r1 !== r2
});
this.state = {
sections: []
active: false,
dataSource: ds.cloneWithRowsAndSections(props.autocompleteChannels)
};
}
componentWillReceiveProps(nextProps) {
const {isSearch, matchTerm, myChannels, otherChannels, privateChannels, publicChannels, requestStatus} = nextProps;
const {isSearch} = nextProps;
const regex = isSearch ? CHANNEL_SEARCH_REGEX : CHANNEL_MENTION_REGEX;
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex);
if ((matchTerm !== this.props.matchTerm && matchTerm === null) || this.state.mentionComplete) {
// if the term changes but is null or the mention has been completed we render this component as null
this.setState({
mentionComplete: false,
sections: []
});
return;
} else if (matchTerm === null) {
// if the terms did not change but is null then we don't need to do anything
// If not match or if user clicked on a channel
if (!match || this.state.mentionComplete) {
const nextState = {
active: false,
mentionComplete: false
};
// Handle the case where the user typed a ~ first and then backspaced
if (nextProps.postDraft.length < this.props.postDraft.length) {
nextState.matchTerm = null;
}
this.setState(nextState);
return;
}
if (matchTerm !== this.props.matchTerm) {
// if the term changed and we haven't made the request do that first
const matchTerm = isSearch ? match[1] : match[2];
const myChannels = this.filter(nextProps.autocompleteChannels.myChannels, matchTerm);
const otherChannels = this.filter(nextProps.autocompleteChannels.otherChannels, matchTerm);
// Show loading indicator on first pull for channels
if (nextProps.requestStatus === RequestStatus.STARTED && ((myChannels.length === 0 && otherChannels.length === 0) || matchTerm === '')) {
this.setState({
active: true,
loading: true
});
return;
}
// Still matching the same term that didn't return any results
let startsWith;
if (isSearch) {
startsWith = match[0].startsWith(`in:${this.state.matchTerm}`) || match[0].startsWith(`channel:${this.state.matchTerm}`);
} else {
startsWith = match[0].startsWith(`~${this.state.matchTerm}`);
}
if (startsWith && (myChannels.length === 0 && otherChannels.length === 0)) {
this.setState({
active: false
});
return;
}
if (matchTerm !== this.state.matchTerm) {
this.setState({
matchTerm
});
const {currentTeamId} = this.props;
this.props.actions.searchChannels(currentTeamId, matchTerm);
return;
}
if (requestStatus !== RequestStatus.STARTED &&
(myChannels !== this.props.myChannels || otherChannels !== this.props.otherChannels ||
privateChannels !== this.props.privateChannels || publicChannels !== this.props.publicChannels)) {
// if the request is complete and the term is not null we show the autocomplete
const sections = [];
if (isSearch) {
if (publicChannels.length) {
sections.push({
id: 'suggestion.search.public',
defaultMessage: 'Public Channels',
data: publicChannels,
key: 'publicChannels'
});
}
if (privateChannels.length) {
sections.push({
id: 'suggestion.search.private',
defaultMessage: 'Private Channels',
data: privateChannels,
key: 'privateChannels'
});
}
} else {
if (myChannels.length) {
sections.push({
id: 'suggestion.mention.channels',
defaultMessage: 'My Channels',
data: myChannels,
key: 'myChannels'
});
}
if (otherChannels.length) {
sections.push({
id: 'suggestion.mention.morechannels',
defaultMessage: 'Other Channels',
data: otherChannels,
key: 'otherChannels'
});
}
if (nextProps.requestStatus !== RequestStatus.STARTED && this.props.autocompleteChannels !== nextProps.autocompleteChannels) {
let data = {};
if (myChannels.length > 0) {
data = Object.assign({}, data, {myChannels});
}
if (otherChannels.length > 0) {
data = Object.assign({}, data, {otherChannels});
}
this.setState({
sections
active: true,
loading: false,
dataSource: this.state.dataSource.cloneWithRowsAndSections(data)
});
}
}
filter = (channels, matchTerm) => {
return channels.filter((c) => c.name.includes(matchTerm) || c.display_name.includes(matchTerm));
};
completeMention = (mention) => {
const {cursorPosition, isSearch, onChangeText, postDraft} = this.props;
const mentionPart = postDraft.substring(0, cursorPosition);
@@ -122,7 +141,7 @@ export default class ChannelMention extends PureComponent {
let completedDraft;
if (isSearch) {
const channelOrIn = mentionPart.includes('in:') ? 'in:' : 'channel:';
completedDraft = mentionPart.replace(CHANNEL_MENTION_SEARCH_REGEX, `${channelOrIn} ${mention} `);
completedDraft = mentionPart.replace(CHANNEL_SEARCH_REGEX, `${channelOrIn} ${mention} `);
} else {
completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `);
}
@@ -131,66 +150,146 @@ export default class ChannelMention extends PureComponent {
completedDraft += postDraft.substring(cursorPosition);
}
onChangeText(completedDraft, true);
this.setState({mentionComplete: true});
onChangeText(completedDraft);
this.setState({
active: false,
mentionComplete: true,
matchTerm: `${mention} `
});
};
keyExtractor = (item) => {
return item.id || item;
};
renderSectionHeader = (sectionData, sectionId) => {
const style = getStyleFromTheme(this.props.theme);
const localization = {
myChannels: {
id: 'suggestion.mention.channels',
defaultMessage: 'My Channels'
},
otherChannels: {
id: 'suggestion.mention.morechannels',
defaultMessage: 'Other Channels'
}
};
renderSectionHeader = ({section}) => {
return (
<AutocompleteSectionHeader
id={section.id}
defaultMessage={section.defaultMessage}
theme={this.props.theme}
/>
<View style={style.sectionWrapper}>
<View style={style.section}>
<FormattedText
id={localization[sectionId].id}
defaultMessage={localization[sectionId].defaultMessage}
style={style.sectionText}
/>
</View>
</View>
);
};
renderItem = ({item}) => {
renderRow = (data) => {
const style = getStyleFromTheme(this.props.theme);
return (
<ChannelMentionItem
channelId={item}
onPress={this.completeMention}
/>
<TouchableOpacity
onPress={() => this.completeMention(data.name)}
style={style.row}
>
<Text style={style.rowDisplayName}>{data.display_name}</Text>
<Text style={style.rowName}>{` (~${data.name})`}</Text>
</TouchableOpacity>
);
};
render() {
const {isSearch, theme} = this.props;
const {mentionComplete, sections} = this.state;
if (sections.length === 0 || mentionComplete) {
// If we are not in an active state or the mention has been completed return null so nothing is rendered
if (!this.state.active) {
// If we are not in an active state return null so nothing is rendered
// other components are not blocked.
return null;
}
const {requestStatus, theme} = this.props;
const style = getStyleFromTheme(theme);
if (this.state.loading && requestStatus === RequestStatus.STARTED) {
return (
<View style={style.loading}>
<FormattedText
id='analytics.chart.loading'
defaultMessage='Loading...'
style={style.sectionText}
/>
</View>
);
}
return (
<SectionList
<ListView
keyboardShouldPersistTaps='always'
keyExtractor={this.keyExtractor}
style={[style.listView, isSearch ? style.search : null]}
sections={sections}
renderItem={this.renderItem}
style={style.listView}
enableEmptySections={true}
dataSource={this.state.dataSource}
renderSectionHeader={this.renderSectionHeader}
initialNumToRender={10}
renderRow={this.renderRow}
pageSize={10}
initialListSize={10}
/>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
listView: {
return StyleSheet.create({
section: {
justifyContent: 'center',
paddingLeft: 8,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
sectionText: {
fontSize: 12,
color: changeOpacity(theme.centerChannelColor, 0.7),
paddingVertical: 7
},
sectionWrapper: {
backgroundColor: theme.centerChannelBg
},
search: {
height: 250
listView: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
loading: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 20,
backgroundColor: theme.centerChannelBg,
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderBottomWidth: 0
},
row: {
padding: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowDisplayName: {
fontSize: 13,
color: theme.centerChannelColor
},
rowName: {
color: theme.centerChannelColor,
opacity: 0.6
}
};
});
});

View File

@@ -5,29 +5,21 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {searchChannels} from 'mattermost-redux/actions/channels';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {General} from 'mattermost-redux/constants';
import {getMyChannels, getOtherChannels} from 'mattermost-redux/selectors/entities/channels';
import {
filterMyChannels,
filterOtherChannels,
filterPublicChannels,
filterPrivateChannels,
getMatchTermForChannelMention
} from 'app/selectors/autocomplete';
import {getTheme} from 'app/selectors/preferences';
import ChannelMention from './channel_mention';
function mapStateToProps(state, ownProps) {
const {cursorPosition, isSearch, rootId} = ownProps;
const currentChannelId = getCurrentChannelId(state);
const {currentChannelId} = state.entities.channels;
let postDraft = '';
if (isSearch) {
let postDraft;
if (ownProps.isSearch) {
postDraft = state.views.search;
} else if (rootId) {
const threadDraft = state.views.thread.drafts[rootId];
} else if (ownProps.rootId) {
const threadDraft = state.views.thread.drafts[ownProps.rootId];
if (threadDraft) {
postDraft = threadDraft.draft;
}
@@ -38,30 +30,17 @@ function mapStateToProps(state, ownProps) {
}
}
const value = postDraft.substring(0, cursorPosition);
const matchTerm = getMatchTermForChannelMention(value, isSearch);
let myChannels;
let otherChannels;
let publicChannels;
let privateChannels;
if (isSearch) {
publicChannels = filterPublicChannels(state, matchTerm);
privateChannels = filterPrivateChannels(state, matchTerm);
} else {
myChannels = filterMyChannels(state, matchTerm);
otherChannels = filterOtherChannels(state, matchTerm);
}
const autocompleteChannels = {
myChannels: getMyChannels(state).filter((c) => c.type !== General.DM_CHANNEL && c.type !== General.GM_CHANNEL),
otherChannels: getOtherChannels(state)
};
return {
...ownProps,
myChannels,
otherChannels,
publicChannels,
privateChannels,
currentTeamId: getCurrentTeamId(state),
matchTerm,
currentChannelId,
currentTeamId: state.entities.teams.currentTeamId,
postDraft,
autocompleteChannels,
requestStatus: state.requests.channels.getChannels.status,
theme: getTheme(state)
};

View File

@@ -1,72 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity
} from 'react-native';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class ChannelMentionItem extends PureComponent {
static propTypes = {
channelId: PropTypes.string.isRequired,
displayName: PropTypes.string,
name: PropTypes.string,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired
};
completeMention = () => {
const {onPress, name} = this.props;
onPress(name);
};
render() {
const {
channelId,
displayName,
name,
theme
} = this.props;
const style = getStyleFromTheme(theme);
return (
<TouchableOpacity
key={channelId}
onPress={this.completeMention}
style={style.row}
>
<Text style={style.rowDisplayName}>{displayName}</Text>
<Text style={style.rowName}>{` (~${name})`}</Text>
</TouchableOpacity>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
padding: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowDisplayName: {
fontSize: 13,
color: theme.centerChannelColor
},
rowName: {
color: theme.centerChannelColor,
opacity: 0.6
}
};
});

View File

@@ -1,23 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'app/selectors/preferences';
import ChannelMentionItem from './channel_mention_item';
function mapStateToProps(state, ownProps) {
const channel = getChannel(state, ownProps.channelId);
return {
displayName: channel.display_name,
name: channel.name,
theme: getTheme(state),
...ownProps
};
}
export default connect(mapStateToProps)(ChannelMentionItem);

View File

@@ -5,6 +5,7 @@ import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
FlatList,
StyleSheet,
Text,
TouchableOpacity,
View
@@ -13,19 +14,15 @@ import {
import Emoji from 'app/components/emoji';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
const EMOJI_REGEX = /(^|\s|^\+|^-)(:([^:\s]*))$/i;
const EMOJI_REGEX = /\B(:([^:\r\n\s]*))$/i;
export default class EmojiSuggestion extends Component {
static propTypes = {
actions: PropTypes.shape({
addReactionToLatestPost: PropTypes.func.isRequired
}).isRequired,
cursorPosition: PropTypes.number,
emojis: PropTypes.array.isRequired,
postDraft: PropTypes.string,
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
rootId: PropTypes.string
onChangeText: PropTypes.func.isRequired
};
static defaultProps = {
@@ -51,7 +48,7 @@ export default class EmojiSuggestion extends Component {
return;
}
const matchTerm = match[3];
const matchTerm = match[2];
if (matchTerm !== this.state.matchTerm) {
this.setState({
matchTerm
@@ -74,22 +71,16 @@ export default class EmojiSuggestion extends Component {
}
completeSuggestion = (emoji) => {
const {actions, cursorPosition, onChangeText, postDraft, rootId} = this.props;
const {cursorPosition, onChangeText, postDraft} = this.props;
const emojiPart = postDraft.substring(0, cursorPosition);
if (emojiPart.startsWith('+:')) {
actions.addReactionToLatestPost(emoji, rootId);
onChangeText('');
} else {
let completedDraft = emojiPart.replace(EMOJI_REGEX, `:${emoji}: `);
let completedDraft = emojiPart.replace(EMOJI_REGEX, `:${emoji}: `);
if (postDraft.length > cursorPosition) {
completedDraft += postDraft.substring(cursorPosition);
}
onChangeText(completedDraft);
if (postDraft.length > cursorPosition) {
completedDraft += postDraft.substring(cursorPosition);
}
onChangeText(completedDraft);
this.setState({
active: false,
emojiComplete: true
@@ -109,7 +100,7 @@ export default class EmojiSuggestion extends Component {
<View style={style.emoji}>
<Emoji
emojiName={item}
size={20}
size={10}
/>
</View>
<Text style={style.emojiName}>{`:${item}:`}</Text>
@@ -144,7 +135,7 @@ export default class EmojiSuggestion extends Component {
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
emoji: {
marginRight: 5
},
@@ -169,5 +160,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
}
};
});
});

View File

@@ -3,11 +3,9 @@
import {connect} from 'react-redux';
import {createSelector} from 'reselect';
import {bindActionCreators} from 'redux';
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
import {addReactionToLatestPost} from 'app/actions/views/emoji';
import {getTheme} from 'app/selectors/preferences';
import {EmojiIndicesByAlias} from 'app/utils/emojis';
@@ -49,12 +47,4 @@ function mapStateToProps(state, ownProps) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
addReactionToLatestPost
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(EmojiSuggestion);
export default connect(mapStateToProps)(EmojiSuggestion);

View File

@@ -1,10 +1,9 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
StyleSheet,
View
} from 'react-native';
@@ -13,7 +12,27 @@ import AtMention from './at_mention';
import ChannelMention from './channel_mention';
import EmojiSuggestion from './emoji_suggestion';
export default class Autocomplete extends PureComponent {
const style = StyleSheet.create({
container: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
maxHeight: 200,
overflow: 'hidden'
},
searchContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
maxHeight: 300,
overflow: 'hidden',
zIndex: 5
}
});
export default class Autocomplete extends Component {
static propTypes = {
onChangeText: PropTypes.func.isRequired,
rootId: PropTypes.string,
@@ -35,10 +54,9 @@ export default class Autocomplete extends PureComponent {
};
render() {
const searchContainer = this.props.isSearch ? style.searchContainer : null;
const container = this.props.isSearch ? null : style.container;
const container = this.props.isSearch ? style.searchContainer : style.container;
return (
<View style={searchContainer}>
<View>
<View style={container}>
<AtMention
cursorPosition={this.state.cursorPosition}
@@ -57,32 +75,3 @@ export default class Autocomplete extends PureComponent {
);
}
}
const style = StyleSheet.create({
container: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
maxHeight: 200,
overflow: 'hidden'
},
searchContainer: {
elevation: 5,
flex: 1,
left: 0,
maxHeight: 250,
overflow: 'hidden',
position: 'absolute',
right: 0,
zIndex: 5,
...Platform.select({
android: {
top: 47
},
ios: {
top: 64
}
})
}
});

View File

@@ -1,106 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import FormattedText from 'app/components/formatted_text';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class SpecialMentionItem extends PureComponent {
static propTypes = {
completeHandle: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
values: PropTypes.object
};
completeMention = () => {
const {onPress, completeHandle} = this.props;
onPress(completeHandle);
};
render() {
const {
defaultMessage,
id,
completeHandle,
theme,
values
} = this.props;
const style = getStyleFromTheme(theme);
return (
<TouchableOpacity
onPress={this.completeMention}
style={style.row}
>
<View style={style.rowPicture}>
<Icon
name='users'
style={style.rowIcon}
/>
</View>
<Text style={style.textWrapper}>
<Text style={style.rowUsername}>{`@${completeHandle}`}</Text>
<Text style={style.rowUsername}>{' - '}</Text>
<FormattedText
id={id}
defaultMessage={defaultMessage}
values={values}
style={style.rowFullname}
/>
</Text>
</TouchableOpacity>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
paddingVertical: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowPicture: {
marginHorizontal: 8,
width: 20,
alignItems: 'center',
justifyContent: 'center'
},
rowIcon: {
color: changeOpacity(theme.centerChannelColor, 0.7),
fontSize: 14
},
rowUsername: {
fontSize: 13,
color: theme.centerChannelColor
},
rowFullname: {
color: theme.centerChannelColor,
flex: 1,
opacity: 0.6
},
textWrapper: {
flex: 1,
flexWrap: 'wrap',
paddingRight: 8
}
};
});

View File

@@ -91,7 +91,7 @@ export default class Badge extends PureComponent {
setTimeout(() => {
this.setNativeProps({
style: {
opacity: 1
display: 'flex'
}
});
}, 100);
@@ -125,7 +125,7 @@ export default class Badge extends PureComponent {
>
<View
ref='badgeContainer'
style={[styles.badge, this.props.style, {opacity: 0}]}
style={[styles.badge, this.props.style, {display: 'none'}]}
>
<View style={styles.wrapper}>
{this.renderText()}

View File

@@ -7,7 +7,6 @@ import {
BackHandler,
InteractionManager,
Keyboard,
StyleSheet,
View
} from 'react-native';
@@ -15,14 +14,13 @@ import Drawer from 'app/components/drawer';
import {alertErrorWithFallback} from 'app/utils/general';
import ChannelsList from './channels_list';
import DrawerSwiper from './drawer_swipper';
import Swiper from './swiper';
import TeamsList from './teams_list';
import {General, WebsocketEvents} from 'mattermost-redux/constants';
import {General} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
const DRAWER_INITIAL_OFFSET = 40;
const DRAWER_LANDSCAPE_OFFSET = 150;
export default class ChannelDrawer extends PureComponent {
static propTypes = {
@@ -37,34 +35,30 @@ export default class ChannelDrawer extends PureComponent {
}).isRequired,
blurPostTextBox: PropTypes.func.isRequired,
children: PropTypes.node,
currentChannelId: PropTypes.string.isRequired,
currentTeamId: PropTypes.string.isRequired,
channels: PropTypes.object,
currentChannel: PropTypes.object,
currentDisplayName: PropTypes.string,
channelMembers: PropTypes.object,
currentTeam: PropTypes.object,
currentUserId: PropTypes.string.isRequired,
isLandscape: PropTypes.bool.isRequired,
isTablet: PropTypes.bool.isRequired,
intl: PropTypes.object.isRequired,
myTeamMembers: PropTypes.object.isRequired,
navigator: PropTypes.object,
teamsCount: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired
};
closeLeftHandle = null;
openLeftHandle = null;
static defaultProps = {
currentTeam: {},
currentChannel: {}
};
state = {
openDrawer: false,
openDrawerOffset: DRAWER_INITIAL_OFFSET
};
swiperIndex = 1;
constructor(props) {
super(props);
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (props.isLandscape || props.isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.state = {
openDrawer: false,
openDrawerOffset
};
}
componentWillMount() {
this.props.actions.getTeams();
}
@@ -72,30 +66,13 @@ export default class ChannelDrawer extends PureComponent {
componentDidMount() {
EventEmitter.on('open_channel_drawer', this.openChannelDrawer);
EventEmitter.on('close_channel_drawer', this.closeChannelDrawer);
EventEmitter.on(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
this.mounted = true;
}
componentWillReceiveProps(nextProps) {
const {isLandscape, isTablet} = this.props;
if (nextProps.isLandscape !== isLandscape || nextProps.isTablet || isTablet) {
if (this.state.openDrawerOffset !== 0) {
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (nextProps.isLandscape || nextProps.isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.setState({openDrawerOffset});
}
}
}
componentWillUnmount() {
EventEmitter.off('open_channel_drawer', this.openChannelDrawer);
EventEmitter.off('close_channel_drawer', this.closeChannelDrawer);
EventEmitter.off(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
this.mounted = false;
}
handleAndroidBack = () => {
@@ -108,13 +85,7 @@ export default class ChannelDrawer extends PureComponent {
};
closeChannelDrawer = () => {
if (this.mounted) {
this.setState({openDrawer: false});
}
};
drawerSwiperRef = (ref) => {
this.drawerSwiper = ref;
this.setState({openDrawer: false});
};
handleDrawerClose = () => {
@@ -125,11 +96,8 @@ export default class ChannelDrawer extends PureComponent {
this.closeLeftHandle = null;
}
if (this.state.openDrawer && this.mounted) {
// The state doesn't get updated if you swipe to close
this.setState({
openDrawer: false
});
if (this.state.openDrawer) {
this.setState({openDrawer: false});
}
};
@@ -140,7 +108,8 @@ export default class ChannelDrawer extends PureComponent {
};
handleDrawerOpen = () => {
if (this.state.openDrawerOffset !== 0) {
this.setState({openDrawer: true});
if (this.state.openDrawerOffset === DRAWER_INITIAL_OFFSET) {
Keyboard.dismiss();
}
@@ -154,13 +123,6 @@ export default class ChannelDrawer extends PureComponent {
if (!this.openLeftHandle) {
this.openLeftHandle = InteractionManager.createInteractionHandle();
}
if (!this.state.openDrawer && this.mounted) {
// The state doesn't get updated if you swipe to open
this.setState({
openDrawer: true
});
}
};
handleDrawerTween = (ratio) => {
@@ -181,28 +143,15 @@ export default class ChannelDrawer extends PureComponent {
};
};
handleUpdateTitle = (channel) => {
let channelName = '';
if (channel.display_name) {
channelName = channel.display_name;
}
this.props.actions.setChannelDisplayName(channelName);
};
openChannelDrawer = () => {
this.props.blurPostTextBox();
if (this.mounted) {
this.setState({
openDrawer: true
});
}
this.setState({openDrawer: true});
};
selectChannel = (channel) => {
const {
actions,
currentChannelId
currentChannel
} = this.props;
const {
@@ -213,63 +162,72 @@ export default class ChannelDrawer extends PureComponent {
viewChannel
} = actions;
markChannelAsRead(channel.id, currentChannel.id);
setChannelLoading();
viewChannel(currentChannel.id);
setChannelDisplayName(channel.display_name);
this.closeChannelDrawer();
InteractionManager.runAfterInteractions(() => {
handleSelectChannel(channel.id);
markChannelAsRead(channel.id, currentChannelId);
if (channel.id !== currentChannelId) {
viewChannel(currentChannelId);
}
});
};
joinChannel = async (channel) => {
const {
actions,
currentTeamId,
currentChannel,
currentDisplayName,
currentTeam,
currentUserId,
intl
} = this.props;
const {
handleSelectChannel,
joinChannel,
makeDirectChannel
makeDirectChannel,
markChannelAsRead,
setChannelDisplayName,
setChannelLoading,
viewChannel
} = actions;
markChannelAsRead(currentChannel.id);
setChannelLoading();
viewChannel(currentChannel.id);
setChannelDisplayName(channel.display_name);
const displayValue = {displayName: channel.display_name};
let result;
if (channel.type === General.DM_CHANNEL) {
result = await makeDirectChannel(channel.id);
const result = await makeDirectChannel(channel.id);
if (result.error) {
const dmFailedMessage = {
id: 'mobile.open_dm.error',
defaultMessage: "We couldn't open a direct message with {displayName}. Please check your connection and try again."
};
setChannelDisplayName(currentDisplayName);
alertErrorWithFallback(intl, result.error, dmFailedMessage, displayValue);
} else {
this.closeChannelDrawer();
}
} else {
result = await joinChannel(currentUserId, currentTeamId, channel.id);
const result = await joinChannel(currentUserId, currentTeam.id, channel.id);
if (result.error) {
const joinFailedMessage = {
id: 'mobile.join_channel.error',
defaultMessage: "We couldn't join the channel {displayName}. Please check your connection and try again."
};
setChannelDisplayName(currentDisplayName);
alertErrorWithFallback(intl, result.error, joinFailedMessage, displayValue);
} else {
this.closeChannelDrawer();
InteractionManager.runAfterInteractions(() => {
handleSelectChannel(channel.id);
});
}
}
if (result.error) {
return;
}
this.selectChannel(result.data);
};
onPageSelected = (index) => {
@@ -278,13 +236,8 @@ export default class ChannelDrawer extends PureComponent {
onSearchEnds = () => {
//hack to update the drawer when the offset changes
const {isLandscape, isTablet} = this.props;
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (isLandscape || isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.setState({openDrawerOffset});
this.setState({openDrawerOffset: DRAWER_INITIAL_OFFSET});
};
onSearchStart = () => {
@@ -293,63 +246,56 @@ export default class ChannelDrawer extends PureComponent {
};
showTeams = () => {
if (this.swiperIndex === 1 && this.props.teamsCount > 1) {
this.drawerSwiper.getWrappedInstance().showTeamsPage();
const teamsCount = Object.keys(this.props.myTeamMembers).length;
if (this.swiperIndex === 1 && teamsCount > 1) {
this.refs.swiper.showTeamsPage();
}
};
resetDrawer = () => {
if (this.swiperIndex !== 1) {
this.drawerSwiper.getWrappedInstance().resetPage();
this.refs.swiper.resetPage();
}
};
renderContent = () => {
const {
currentChannel,
currentTeam,
channels,
channelMembers,
navigator,
teamsCount,
myTeamMembers,
theme
} = this.props;
const {openDrawerOffset} = this.state;
const showTeams = openDrawerOffset === DRAWER_INITIAL_OFFSET && Object.keys(myTeamMembers).length > 1;
const {
openDrawerOffset
} = this.state;
const multipleTeams = teamsCount > 1;
const showTeams = openDrawerOffset !== 0 && multipleTeams;
if (this.drawerSwiper) {
if (multipleTeams) {
this.drawerSwiper.getWrappedInstance().runOnLayout();
} else if (!openDrawerOffset) {
this.drawerSwiper.getWrappedInstance().scrollToStart();
}
}
const lists = [];
if (multipleTeams) {
const teamsList = (
<View
key='teamsList'
style={style.swiperContent}
>
let teams;
if (showTeams) {
teams = (
<View style={{flex: 1, marginBottom: 10}}>
<TeamsList
closeChannelDrawer={this.closeChannelDrawer}
myTeamMembers={myTeamMembers}
navigator={navigator}
/>
</View>
);
lists.push(teamsList);
}
lists.push(
<View
key='channelsList'
style={style.swiperContent}
>
const channelsList = (
<View style={{flex: 1, marginBottom: 10}}>
<ChannelsList
navigator={navigator}
currentTeam={currentTeam}
currentChannel={currentChannel}
channels={channels}
channelMembers={channelMembers}
myTeamMembers={myTeamMembers}
theme={theme}
onSelectChannel={this.selectChannel}
onJoinChannel={this.joinChannel}
navigator={navigator}
onShowTeams={this.showTeams}
onSearchStart={this.onSearchStart}
onSearchEnds={this.onSearchEnds}
@@ -358,15 +304,16 @@ export default class ChannelDrawer extends PureComponent {
);
return (
<DrawerSwiper
ref={this.drawerSwiperRef}
<Swiper
ref='swiper'
onPageSelected={this.onPageSelected}
openDrawerOffset={openDrawerOffset}
showTeams={showTeams}
theme={theme}
>
{lists}
</DrawerSwiper>
{teams}
{channelsList}
</Swiper>
);
};
@@ -380,12 +327,11 @@ export default class ChannelDrawer extends PureComponent {
open={openDrawer}
onOpenStart={this.handleDrawerOpenStart}
onOpen={this.handleDrawerOpen}
onClose={this.handleDrawerClose}
onCloseStart={this.handleDrawerCloseStart}
onClose={this.handleDrawerClose}
captureGestures='open'
type='static'
acceptTap={true}
acceptPanOnDrawer={true}
disabled={false}
content={this.renderContent()}
tapToClose={true}
@@ -417,10 +363,3 @@ export default class ChannelDrawer extends PureComponent {
);
}
}
const style = StyleSheet.create({
swiperContent: {
flex: 1,
marginBottom: 10
}
});

View File

@@ -4,12 +4,12 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
TouchableHighlight,
Text,
View
} from 'react-native';
import Badge from 'app/components/badge';
import ChanneIcon from 'app/components/channel_icon';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -26,9 +26,9 @@ export default class ChannelItem extends PureComponent {
onPress = () => {
const {channel, onSelectChannel} = this.props;
requestAnimationFrame(() => {
setTimeout(() => {
preventDoubleTap(onSelectChannel, this, channel);
});
}, 100);
};
render() {
@@ -50,14 +50,11 @@ export default class ChannelItem extends PureComponent {
if (mentions && !isActive) {
badge = (
<Badge
style={style.badge}
countStyle={style.mention}
count={mentions}
minHeight={20}
minWidth={20}
onPress={this.onPress}
/>
<View style={style.badgeContainer}>
<Text style={style.badge}>
{mentions}
</Text>
</View>
);
}
@@ -111,7 +108,7 @@ export default class ChannelItem extends PureComponent {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
@@ -146,18 +143,19 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
textUnread: {
color: theme.sidebarUnreadText
},
badge: {
badgeContainer: {
alignItems: 'center',
backgroundColor: theme.mentionBj,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
padding: 3,
position: 'relative',
right: 16
borderRadius: 7,
height: 15,
justifyContent: 'center',
marginRight: 16,
width: 16
},
mention: {
badge: {
color: theme.mentionColor,
fontSize: 10
fontSize: 10,
fontWeight: '600'
}
};
});
});

View File

@@ -1,304 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {
Platform,
TouchableHighlight,
View
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import SearchBar from 'app/components/search_bar';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import FilteredList from './filtered_list';
import List from './list';
import SwitchTeams from './switch_teams';
class ChannelsList extends React.PureComponent {
static propTypes = {
intl: intlShape.isRequired,
navigator: PropTypes.object,
onJoinChannel: PropTypes.func.isRequired,
onSearchEnds: PropTypes.func.isRequired,
onSearchStart: PropTypes.func.isRequired,
onSelectChannel: PropTypes.func.isRequired,
onShowTeams: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired
};
constructor(props) {
super(props);
this.firstUnreadChannel = null;
this.state = {
searching: false,
term: ''
};
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
this.closeButton = source;
});
}
onSelectChannel = (channel) => {
if (channel.fake) {
this.props.onJoinChannel(channel);
} else {
this.props.onSelectChannel(channel);
}
this.refs.search_bar.cancel();
};
openSettingsModal = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
screen: 'Settings',
title: intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}),
animationType: 'slide-up',
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
},
navigatorButtons: {
leftButtons: [{
id: 'close-settings',
icon: this.closeButton
}]
}
});
});
onSearch = (term) => {
this.setState({term});
};
onSearchFocused = () => {
this.setState({searching: true});
this.props.onSearchStart();
};
cancelSearch = () => {
this.props.onSearchEnds();
this.setState({searching: false});
this.onSearch('');
};
render() {
const {
intl,
navigator,
onShowTeams,
theme
} = this.props;
const {searching, term} = this.state;
const styles = getStyleSheet(theme);
let settings;
let list;
if (searching) {
list = (
<FilteredList
onSelectChannel={this.onSelectChannel}
styles={styles}
term={term}
/>
);
} else {
settings = (
<TouchableHighlight
style={styles.settingsContainer}
onPress={this.openSettingsModal}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<AwesomeIcon
name='cog'
style={styles.settings}
/>
</TouchableHighlight>
);
list = (
<List
navigator={navigator}
onSelectChannel={this.onSelectChannel}
styles={styles}
/>
);
}
const title = (
<View style={styles.searchContainer}>
<SearchBar
ref='search_bar'
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Jump to a conversation'})}
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={{
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.2),
color: theme.sidebarHeaderTextColor,
fontSize: 13
}}
placeholderTextColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.8)}
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
titleCancelColor={theme.sidebarHeaderTextColor}
selectionColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
onSearchButtonPress={this.onSearch}
onCancelButtonPress={this.cancelSearch}
onChangeText={this.onSearch}
onFocus={this.onSearchFocused}
value={term}
/>
</View>
);
return (
<View
style={styles.container}
>
<View style={styles.statusBar}>
<View style={styles.headerContainer}>
<SwitchTeams
searching={searching}
showTeams={onShowTeams}
/>
{title}
{settings}
</View>
</View>
{list}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
backgroundColor: theme.sidebarBg,
flex: 1
},
statusBar: {
backgroundColor: theme.sidebarHeaderBg,
...Platform.select({
ios: {
paddingTop: 20
}
})
},
headerContainer: {
alignItems: 'center',
paddingLeft: 10,
backgroundColor: theme.sidebarHeaderBg,
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: changeOpacity(theme.sidebarHeaderTextColor, 0.10),
...Platform.select({
android: {
height: 46
},
ios: {
height: 44
}
})
},
header: {
color: theme.sidebarHeaderTextColor,
flex: 1,
fontSize: 17,
fontWeight: 'normal',
paddingLeft: 16
},
settingsContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 10,
...Platform.select({
android: {
height: 46,
marginRight: 6
},
ios: {
height: 44,
marginRight: 8
}
})
},
settings: {
color: theme.sidebarHeaderTextColor,
fontSize: 18,
fontWeight: '300'
},
titleContainer: { // These aren't used by this component, but they are passed down to the list component
alignItems: 'center',
flex: 1,
flexDirection: 'row',
height: 48,
marginLeft: 16
},
title: {
flex: 1,
color: theme.sidebarText,
opacity: 1,
fontSize: 15,
fontWeight: '400',
letterSpacing: 0.8,
lineHeight: 18
},
searchContainer: {
flex: 1,
...Platform.select({
android: {
marginBottom: 1
},
ios: {
marginBottom: 3
}
})
},
divider: {
backgroundColor: changeOpacity(theme.sidebarText, 0.1),
height: 1
},
actionContainer: {
alignItems: 'center',
height: 48,
justifyContent: 'center',
width: 50
},
action: {
color: theme.sidebarText,
fontSize: 20,
fontWeight: '500',
lineHeight: 18
},
above: {
backgroundColor: theme.mentionBj,
top: 9
},
indicatorText: {
backgroundColor: 'transparent',
color: theme.mentionColor,
fontSize: 14,
paddingVertical: 2,
paddingHorizontal: 4,
textAlign: 'center',
textAlignVertical: 'center'
}
};
});
export default injectIntl(ChannelsList);

View File

@@ -13,6 +13,7 @@ import {
import {injectIntl, intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity} from 'app/utils/theme';
import {General} from 'mattermost-redux/constants';
@@ -33,6 +34,7 @@ class FilteredList extends Component {
currentTeam: PropTypes.object.isRequired,
currentUserId: PropTypes.string,
currentChannel: PropTypes.object,
groupChannels: PropTypes.array,
groupChannelMemberDetails: PropTypes.object,
intl: intlShape.isRequired,
teammateNameDisplay: PropTypes.string,
@@ -323,7 +325,7 @@ class FilteredList extends Component {
return (
<TouchableHighlight
style={styles.actionContainer}
onPress={action}
onPress={() => preventDoubleTap(action, this)}
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
>
<MaterialIcon

View File

@@ -9,15 +9,10 @@ import {searchChannels} from 'mattermost-redux/actions/channels';
import {getProfilesInTeam, searchProfiles} from 'mattermost-redux/actions/users';
import {makeGroupMessageVisibleIfNecessary} from 'mattermost-redux/actions/preferences';
import {General} from 'mattermost-redux/constants';
import {
getChannelsWithUnreadSection,
getCurrentChannel,
getGroupChannels,
getOtherChannels
} from 'mattermost-redux/selectors/entities/channels';
import {getGroupChannels, getOtherChannels} from 'mattermost-redux/selectors/entities/channels';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUserId, getProfilesInCurrentTeam, getUsers, getUserIdsInChannels, getUserStatuses} from 'mattermost-redux/selectors/entities/users';
import {getDirectShowPreferences, getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getDirectShowPreferences, getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import Config from 'assets/config';
@@ -107,10 +102,9 @@ function mapStateToProps(state, ownProps) {
const searchOrder = Config.DrawerSearchOrder ? Config.DrawerSearchOrder : DEFAULT_SEARCH_ORDER;
return {
channels: getChannelsWithUnreadSection(state),
currentChannel: getCurrentChannel(state),
currentUserId,
otherChannels: getOtherChannels(state),
groupChannels: getGroupChannels(state),
groupChannelMemberDetails: getGroupChannelMemberDetails(state),
profiles,
teamProfiles,
@@ -119,7 +113,6 @@ function mapStateToProps(state, ownProps) {
searchOrder,
pastDirectMessages: pastDirectMessages(state),
restrictDms,
theme: getTheme(state),
...ownProps
};
}

View File

@@ -1,17 +1,407 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
StyleSheet,
Text,
TouchableHighlight,
View
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {getTheme} from 'app/selectors/preferences';
import Badge from 'app/components/badge';
import SearchBar from 'app/components/search_bar';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import ChannelsList from './channels_list';
import FilteredList from './filtered_list';
import List from './list';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
theme: getTheme(state)
class ChannelsList extends Component {
static propTypes = {
channels: PropTypes.object.isRequired,
channelMembers: PropTypes.object,
currentTeam: PropTypes.object.isRequired,
currentChannel: PropTypes.object,
intl: intlShape.isRequired,
myTeamMembers: PropTypes.object.isRequired,
navigator: PropTypes.object,
onJoinChannel: PropTypes.func.isRequired,
onSearchEnds: PropTypes.func.isRequired,
onSearchStart: PropTypes.func.isRequired,
onSelectChannel: PropTypes.func.isRequired,
onShowTeams: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired
};
static defaultProps = {
currentTeam: {},
currentChannel: {}
};
constructor(props) {
super(props);
this.firstUnreadChannel = null;
this.state = {
searching: false,
term: ''
};
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
this.closeButton = source;
});
}
onSelectChannel = (channel) => {
if (channel.fake) {
this.props.onJoinChannel(channel);
} else {
this.props.onSelectChannel(channel);
}
this.refs.search_bar.cancel();
};
openSettingsModal = () => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
screen: 'Settings',
title: intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}),
animationType: 'slide-up',
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
},
navigatorButtons: {
leftButtons: [{
id: 'close-settings',
icon: this.closeButton
}]
}
});
};
onSearch = (term) => {
this.setState({term});
};
onSearchFocused = () => {
this.setState({searching: true});
this.props.onSearchStart();
};
cancelSearch = () => {
this.props.onSearchEnds();
this.setState({searching: false});
this.onSearch('');
};
render() {
const {
currentChannel,
currentTeam,
intl,
myTeamMembers,
onShowTeams,
theme
} = this.props;
if (!currentChannel) {
return <Text>{'Loading'}</Text>;
}
const {searching, term} = this.state;
const teamMembers = Object.values(myTeamMembers);
const showMembers = teamMembers.length > 1;
const styles = getStyleSheet(theme);
let settings;
let list;
if (searching) {
const listProps = {...this.props, onSelectChannel: this.onSelectChannel, styles, term};
list = <FilteredList {...listProps}/>;
} else {
settings = (
<TouchableHighlight
style={styles.settingsContainer}
onPress={() => preventDoubleTap(this.openSettingsModal)}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<AwesomeIcon
name='cog'
style={styles.settings}
/>
</TouchableHighlight>
);
const listProps = {...this.props, onSelectChannel: this.onSelectChannel, styles};
list = <List {...listProps}/>;
}
const title = (
<View style={styles.searchContainer}>
<SearchBar
ref='search_bar'
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Jump to a conversation'})}
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={{
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.2),
color: theme.sidebarHeaderTextColor,
fontSize: 13
}}
placeholderTextColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.8)}
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
titleCancelColor={theme.sidebarHeaderTextColor}
selectionColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
onSearchButtonPress={this.onSearch}
onCancelButtonPress={this.cancelSearch}
onChangeText={this.onSearch}
onFocus={this.onSearchFocused}
value={term}
/>
</View>
);
let badge;
let switcher;
if (showMembers && !searching) {
let mentionCount = 0;
let messageCount = 0;
teamMembers.forEach((m) => {
if (m.team_id !== currentTeam.id) {
mentionCount = mentionCount + (m.mention_count || 0);
messageCount = messageCount + (m.msg_count || 0);
}
});
let badgeCount = 0;
if (mentionCount) {
badgeCount = mentionCount;
} else if (messageCount) {
badgeCount = -1;
}
if (badgeCount) {
badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={badgeCount}
minHeight={20}
minWidth={20}
/>
);
}
switcher = (
<TouchableHighlight
onPress={() => preventDoubleTap(onShowTeams)}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<View style={styles.switcherContainer}>
<AwesomeIcon
name='chevron-left'
size={12}
color={theme.sidebarHeaderBg}
/>
<View style={styles.switcherDivider}/>
<Text style={styles.switcherTeam}>
{currentTeam.display_name.substr(0, 2).toUpperCase()}
</Text>
</View>
</TouchableHighlight>
);
}
return (
<View
style={[styles.container, showMembers ? styles.extraPadding : {}]}
>
<View style={styles.statusBar}>
<View style={styles.headerContainer}>
{switcher}
{title}
{settings}
{badge}
</View>
</View>
{list}
</View>
);
}
}
export default connect(mapStateToProps)(ChannelsList);
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
backgroundColor: theme.sidebarBg,
flex: 1
},
extraPadding: {
...Platform.select({
ios: {
paddingBottom: 10
}
})
},
statusBar: {
backgroundColor: theme.sidebarHeaderBg,
...Platform.select({
ios: {
paddingTop: 20
}
})
},
headerContainer: {
alignItems: 'center',
paddingLeft: 10,
backgroundColor: theme.sidebarHeaderBg,
flexDirection: 'row',
borderBottomWidth: 1,
borderBottomColor: changeOpacity(theme.sidebarHeaderTextColor, 0.10),
...Platform.select({
android: {
height: 46
},
ios: {
height: 44
}
})
},
header: {
color: theme.sidebarHeaderTextColor,
flex: 1,
fontSize: 17,
fontWeight: 'normal',
paddingLeft: 16
},
settingsContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 10,
...Platform.select({
android: {
height: 46,
marginRight: 6
},
ios: {
height: 44,
marginRight: 8
}
})
},
settings: {
color: theme.sidebarHeaderTextColor,
fontSize: 18,
fontWeight: '300'
},
titleContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
height: 48,
marginLeft: 16
},
title: {
flex: 1,
color: theme.sidebarText,
opacity: 1,
fontSize: 15,
fontWeight: '400',
letterSpacing: 0.8,
lineHeight: 18
},
searchContainer: {
flex: 1,
...Platform.select({
android: {
marginBottom: 1
},
ios: {
marginBottom: 3
}
})
},
switcherContainer: {
alignItems: 'center',
backgroundColor: theme.sidebarHeaderTextColor,
borderRadius: 2,
flexDirection: 'row',
height: 32,
justifyContent: 'center',
marginLeft: 6,
marginRight: 10,
paddingHorizontal: 6
},
switcherDivider: {
backgroundColor: theme.sidebarHeaderBg,
height: 15,
marginHorizontal: 6,
width: 1
},
switcherTeam: {
color: theme.sidebarHeaderBg,
fontFamily: 'OpenSans',
fontSize: 14
},
badge: {
backgroundColor: theme.mentionBj,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
flexDirection: 'row',
padding: 3,
position: 'absolute',
left: 5,
top: 0
},
mention: {
color: theme.mentionColor,
fontSize: 10
},
divider: {
backgroundColor: changeOpacity(theme.sidebarText, 0.1),
height: 1
},
actionContainer: {
alignItems: 'center',
height: 48,
justifyContent: 'center',
width: 50
},
action: {
color: theme.sidebarText,
fontSize: 20,
fontWeight: '500',
lineHeight: 18
},
above: {
backgroundColor: theme.mentionBj,
top: 9
},
indicatorText: {
backgroundColor: 'transparent',
color: theme.mentionColor,
fontSize: 14,
paddingVertical: 2,
paddingHorizontal: 4,
textAlign: 'center',
textAlignVertical: 'center'
}
});
});
export default injectIntl(ChannelsList);

View File

@@ -4,9 +4,7 @@
import {connect} from 'react-redux';
import {General} from 'mattermost-redux/constants';
import {getChannelsWithUnreadSection, getCurrentChannel, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {showCreateOption} from 'mattermost-redux/utils/channel_utils';
import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
@@ -18,10 +16,6 @@ function mapStateToProps(state, ownProps) {
return {
canCreatePrivateChannels: showCreateOption(config, license, General.PRIVATE_CHANNEL, isAdmin(roles), isSystemAdmin(roles)),
channelMembers: getMyChannelMemberships(state),
channels: getChannelsWithUnreadSection(state),
currentChannel: getCurrentChannel(state),
theme: getTheme(state),
...ownProps
};
}

View File

@@ -14,7 +14,7 @@ import {injectIntl, intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FormattedText from 'app/components/formatted_text';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity} from 'app/utils/theme';
import {General} from 'mattermost-redux/constants';
@@ -151,7 +151,7 @@ class List extends Component {
);
};
createPrivateChannel = wrapWithPreventDoubleTap(() => {
createPrivateChannel = () => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
@@ -171,7 +171,7 @@ class List extends Component {
closeButton: this.closeButton
}
});
});
};
buildChannels = (props) => {
const {canCreatePrivateChannels, styles} = props;
@@ -233,15 +233,7 @@ class List extends Component {
return data;
};
scrollToTop = () => {
this.refs.list.scrollToOffset({
x: 0,
y: 0,
animated: true
});
}
showDirectMessagesModal = wrapWithPreventDoubleTap(() => {
showDirectMessagesModal = () => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
@@ -263,9 +255,9 @@ class List extends Component {
}]
}
});
});
};
showMoreChannelsModal = wrapWithPreventDoubleTap(() => {
showMoreChannelsModal = () => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
@@ -284,14 +276,14 @@ class List extends Component {
closeButton: this.closeButton
}
});
});
};
renderSectionAction = (styles, action) => {
const {theme} = this.props;
return (
<TouchableHighlight
style={styles.actionContainer}
onPress={action}
onPress={() => preventDoubleTap(action, this)}
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
>
<MaterialIcon
@@ -348,7 +340,7 @@ class List extends Component {
above = (
<UnreadIndicator
style={[styles.above, {width: (this.width - 40)}]}
onPress={this.scrollToTop}
onPress={() => this.refs.list.scrollToOffset({x: 0, y: 0, animated: true})}
text={(
<FormattedText
style={styles.indicatorText}

View File

@@ -1,20 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import SwitchTeams from './switch_teams';
function mapStateToProps(state, ownProps) {
return {
currentTeam: getCurrentTeam(state),
teamMembers: getTeamMemberships(state),
theme: getTheme(state),
...ownProps
};
}
export default connect(mapStateToProps)(SwitchTeams);

View File

@@ -1,172 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {
Text,
TouchableHighlight,
View
} from 'react-native';
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import Badge from 'app/components/badge';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class SwitchTeams extends React.PureComponent {
static propTypes = {
currentTeam: PropTypes.object,
searching: PropTypes.bool.isRequired,
showTeams: PropTypes.func.isRequired,
teamMembers: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
constructor(props) {
super(props);
this.state = {
badgeCount: this.getBadgeCount(props)
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.currentTeam !== this.props.currentTeam || nextProps.teamMembers !== this.props.teamMembers) {
this.setState({
badgeCount: this.getBadgeCount(nextProps)
});
}
}
getBadgeCount = (props) => {
const {
currentTeam,
teamMembers
} = props;
let mentionCount = 0;
let messageCount = 0;
Object.values(teamMembers).forEach((m) => {
if (m.team_id !== currentTeam.id) {
mentionCount = mentionCount + (m.mention_count || 0);
messageCount = messageCount + (m.msg_count || 0);
}
});
let badgeCount;
if (mentionCount) {
badgeCount = mentionCount;
} else if (messageCount) {
badgeCount = -1;
} else {
badgeCount = 0;
}
return badgeCount;
};
showTeams = wrapWithPreventDoubleTap(() => {
this.props.showTeams();
});
render() {
const {
currentTeam,
searching,
teamMembers,
theme
} = this.props;
if (!currentTeam) {
return null;
}
const {
badgeCount
} = this.state;
if (searching || Object.keys(teamMembers).length < 2) {
return null;
}
const styles = getStyleSheet(theme);
let badge;
if (badgeCount) {
badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={badgeCount}
minHeight={20}
minWidth={20}
/>
);
}
return (
<View>
<TouchableHighlight
onPress={this.showTeams}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<View style={styles.switcherContainer}>
<AwesomeIcon
name='chevron-left'
size={12}
color={theme.sidebarHeaderBg}
/>
<View style={styles.switcherDivider}/>
<Text style={styles.switcherTeam}>
{currentTeam.display_name.substr(0, 2).toUpperCase()}
</Text>
</View>
</TouchableHighlight>
{badge}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
switcherContainer: {
alignItems: 'center',
backgroundColor: theme.sidebarHeaderTextColor,
borderRadius: 2,
flexDirection: 'row',
height: 32,
justifyContent: 'center',
marginLeft: 6,
marginRight: 10,
paddingHorizontal: 6
},
switcherDivider: {
backgroundColor: theme.sidebarHeaderBg,
height: 15,
marginHorizontal: 6,
width: 1
},
switcherTeam: {
color: theme.sidebarHeaderBg,
fontFamily: 'OpenSans',
fontSize: 14
},
badge: {
backgroundColor: theme.mentionBj,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
flexDirection: 'row',
padding: 3,
position: 'absolute',
left: -5,
top: -5
},
mention: {
color: theme.mentionColor,
fontSize: 10
}
};
});

View File

@@ -1,18 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getDimensions, isLandscape} from 'app/selectors/device';
import DraweSwiper from './drawer_swiper';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
...getDimensions(state),
isLandscape: isLandscape(state)
};
}
export default connect(mapStateToProps, null, null, {withRef: true})(DraweSwiper);

View File

@@ -6,12 +6,11 @@ import {connect} from 'react-redux';
import {joinChannel, viewChannel, markChannelAsRead} from 'mattermost-redux/actions/channels';
import {getTeams} from 'mattermost-redux/actions/teams';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import {getChannelsWithUnreadSection, getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import {handleSelectChannel, setChannelDisplayName, setChannelLoading} from 'app/actions/views/channel';
import {makeDirectChannel} from 'app/actions/views/more_dms';
import {isLandscape, isTablet} from 'app/selectors/device';
import {getTheme} from 'app/selectors/preferences';
import ChannelDrawer from './channel_drawer.js';
@@ -21,12 +20,13 @@ function mapStateToProps(state, ownProps) {
return {
...ownProps,
currentTeamId: getCurrentTeamId(state),
currentChannelId: getCurrentChannelId(state),
currentTeam: getCurrentTeam(state) || {},
currentChannel: getCurrentChannel(state) || {},
currentDisplayName: state.views.channel.displayName,
currentUserId,
isLandscape: isLandscape(state),
isTablet: isTablet(state),
teamsCount: Object.keys(getTeamMemberships(state)).length,
channels: getChannelsWithUnreadSection(state),
channelMembers: state.entities.channels.myMembers,
myTeamMembers: getTeamMemberships(state),
theme: getTheme(state)
};
}

View File

@@ -2,5 +2,5 @@
// See License.txt for license information.
// Used to leverage the platform specific components
import MattermostManaged from './mattermost-managed';
export default MattermostManaged;
import Swiper from './swiper';
export default Swiper;

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {ViewPagerAndroid} from 'react-native';
export default class SwiperAndroid extends PureComponent {
static propTypes = {
children: PropTypes.node.isRequired,
onPageSelected: PropTypes.func,
showTeams: PropTypes.bool.isRequired,
theme: PropTypes.object.isRequired
};
static defaultProps = {
onPageSelected: () => true
};
swiperPageSelected = (event) => {
this.props.onPageSelected(event.nativeEvent.position);
};
showTeamsPage = () => {
this.refs.swiper.setPage(0);
this.props.onPageSelected(0);
};
resetPage = () => {
this.refs.swiper.setPageWithoutAnimation(1);
this.props.onPageSelected(1);
};
render() {
const {
children,
showTeams,
theme
} = this.props;
return (
<ViewPagerAndroid
ref='swiper'
style={{flex: 1, backgroundColor: theme.sidebarBg}}
initialPage={1}
scrollEnabled={showTeams}
onPageSelected={this.swiperPageSelected}
>
{children}
</ViewPagerAndroid>
);
}
}

View File

@@ -3,16 +3,14 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {StyleSheet} from 'react-native';
import {Dimensions} from 'react-native';
import Swiper from 'react-native-swiper';
import {changeOpacity} from 'app/utils/theme';
import Swiper from 'app/components/swiper';
export default class DrawerSwiper extends PureComponent {
export default class SwiperIos extends PureComponent {
static propTypes = {
children: PropTypes.node.isRequired,
deviceWidth: PropTypes.number.isRequired,
onPageSelected: PropTypes.func,
openDrawerOffset: PropTypes.number,
showTeams: PropTypes.bool.isRequired,
@@ -24,47 +22,49 @@ export default class DrawerSwiper extends PureComponent {
openDrawerOffset: 0
};
runOnLayout = (shouldRun = true) => {
this.refs.swiper.runOnLayout = shouldRun;
};
resetPage = () => {
this.refs.swiper.scrollToIndex(1, false);
};
scrollToStart = () => {
this.refs.swiper.scrollToStart();
};
swiperPageSelected = (index) => {
this.props.onPageSelected(index);
swiperPageSelected = (e, state, context) => {
this.props.onPageSelected(context.state.index);
};
showTeamsPage = () => {
this.refs.swiper.scrollToIndex(0, true);
this.refs.swiper.scrollBy(-1, true);
};
resetPage = () => {
this.refs.swiper.scrollBy(1, false);
};
render() {
const {
children,
deviceWidth,
openDrawerOffset,
showTeams,
theme
} = this.props;
const initialPage = React.Children.count(children) - 1;
const pagination = {bottom: 0};
if (showTeams) {
pagination.bottom = 10;
}
// Get the dimensions here so when the orientation changes we get the right dimensions
const {height: deviceHeight, width: deviceWidth} = Dimensions.get('window');
return (
<Swiper
ref='swiper'
initialPage={initialPage}
onIndexChanged={this.swiperPageSelected}
paginationStyle={style.pagination}
horizontal={true}
loop={false}
index={1}
onMomentumScrollEnd={this.swiperPageSelected}
paginationStyle={[{position: 'relative'}, pagination]}
width={deviceWidth - openDrawerOffset}
height={deviceHeight}
style={{backgroundColor: theme.sidebarBg}}
activeDotColor={theme.sidebarText}
dotColor={changeOpacity(theme.sidebarText, 0.5)}
removeClippedSubviews={true}
automaticallyAdjustContentInsets={true}
scrollEnabled={showTeams}
showsPagination={showTeams}
keyboardShouldPersistTaps={'always'}
@@ -74,10 +74,3 @@ export default class DrawerSwiper extends PureComponent {
);
}
}
const style = StyleSheet.create({
pagination: {
bottom: 0,
position: 'absolute'
}
});

View File

@@ -4,22 +4,36 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {markChannelAsRead} from 'mattermost-redux/actions/channels';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getCurrentTeamId, getJoinableTeams} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentTeamId, getJoinableTeams, getMyTeams} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {handleTeamChange} from 'app/actions/views/select_team';
import {getTheme} from 'app/selectors/preferences';
import {getMySortedTeams} from 'app/selectors/teams';
import {removeProtocol} from 'app/utils/url';
import TeamsList from './teams_list';
function mapStateToProps(state, ownProps) {
const user = getCurrentUser(state);
function sortTeams(locale, a, b) {
if (a.display_name !== b.display_name) {
return a.display_name.toLowerCase().localeCompare(b.display_name.toLowerCase(), locale, {numeric: true});
}
return a.name.toLowerCase().localeCompare(b.name.toLowerCase(), locale, {numeric: true});
}
return {
canCreateTeams: false,
joinableTeams: getJoinableTeams(state),
currentChannelId: getCurrentChannelId(state),
currentTeamId: getCurrentTeamId(state),
currentUrl: removeProtocol(getCurrentUrl(state)),
teams: getMySortedTeams(state),
teams: getMyTeams(state).sort(sortTeams.bind(null, (user.locale))),
theme: getTheme(state),
...ownProps
};
@@ -28,7 +42,8 @@ function mapStateToProps(state, ownProps) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
handleTeamChange
handleTeamChange,
markChannelAsRead
}, dispatch)
};
}

View File

@@ -4,31 +4,37 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
InteractionManager,
FlatList,
Platform,
StyleSheet,
Text,
TouchableHighlight,
View
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import IonIcon from 'react-native-vector-icons/Ionicons';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import Badge from 'app/components/badge';
import FormattedText from 'app/components/formatted_text';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import TeamsListItem from './teams_list_item';
class TeamsList extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
handleTeamChange: PropTypes.func.isRequired
handleTeamChange: PropTypes.func.isRequired,
markChannelAsRead: PropTypes.func.isRequired
}).isRequired,
canCreateTeams: PropTypes.bool.isRequired,
closeChannelDrawer: PropTypes.func.isRequired,
currentChannelId: PropTypes.string,
currentTeamId: PropTypes.string.isRequired,
currentUrl: PropTypes.string.isRequired,
intl: intlShape.isRequired,
joinableTeams: PropTypes.object.isRequired,
myTeamMembers: PropTypes.object.isRequired,
navigator: PropTypes.object.isRequired,
teams: PropTypes.array.isRequired,
theme: PropTypes.object.isRequired
@@ -37,23 +43,28 @@ class TeamsList extends PureComponent {
constructor(props) {
super(props);
MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).then((source) => {
MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).
then((source) => {
this.closeButton = source;
});
}
selectTeam = (team) => {
requestAnimationFrame(() => {
const {actions, closeChannelDrawer, currentTeamId} = this.props;
if (team.id !== currentTeamId) {
actions.handleTeamChange(team);
}
const {actions, closeChannelDrawer, currentChannelId, currentTeamId} = this.props;
if (team.id === currentTeamId) {
closeChannelDrawer();
});
} else {
if (currentChannelId) {
actions.markChannelAsRead(currentChannelId);
}
closeChannelDrawer();
InteractionManager.runAfterInteractions(() => {
actions.handleTeamChange(team);
});
}
};
goToSelectTeam = wrapWithPreventDoubleTap(() => {
goToSelectTeam = () => {
const {currentUrl, intl, navigator, theme} = this.props;
navigator.showModal({
@@ -79,18 +90,83 @@ class TeamsList extends PureComponent {
theme
}
});
});
keyExtractor = (team) => {
return team.id;
}
};
renderItem = ({item}) => {
const {currentTeamId, currentUrl, myTeamMembers, theme} = this.props;
const styles = getStyleSheet(theme);
let current;
let badge;
if (item.id === currentTeamId) {
current = (
<View style={styles.checkmarkContainer}>
<IonIcon
name='md-checkmark'
style={styles.checkmark}
/>
</View>
);
}
const member = myTeamMembers[item.id];
let badgeCount = 0;
if (member.mention_count) {
badgeCount = member.mention_count;
} else if (member.msg_count) {
badgeCount = -1;
}
if (badgeCount) {
badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={badgeCount}
minHeight={20}
minWidth={20}
/>
);
}
return (
<TeamsListItem
selectTeam={this.selectTeam}
team={item}
/>
<View style={styles.teamWrapper}>
<TouchableHighlight
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={() => {
setTimeout(() => {
preventDoubleTap(this.selectTeam, this, item);
}, 100);
}}
>
<View style={styles.teamContainer}>
<View style={styles.teamIconContainer}>
<Text style={styles.teamIcon}>
{item.display_name.substr(0, 2).toUpperCase()}
</Text>
</View>
<View style={styles.teamNameContainer}>
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={styles.teamName}
>
{item.display_name}
</Text>
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={styles.teamUrl}
>
{`${currentUrl}/${item.name}`}
</Text>
</View>
{current}
</View>
</TouchableHighlight>
{badge}
</View>
);
};
@@ -103,7 +179,7 @@ class TeamsList extends PureComponent {
moreAction = (
<TouchableHighlight
style={styles.moreActionContainer}
onPress={this.goToSelectTeam}
onPress={() => preventDoubleTap(this.goToSelectTeam)}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<Text
@@ -130,7 +206,7 @@ class TeamsList extends PureComponent {
<FlatList
data={teams}
renderItem={this.renderItem}
keyExtractor={this.keyExtractor}
keyExtractor={(item) => item.id}
viewabilityConfig={{
viewAreaCoveragePercentThreshold: 3,
waitForInteraction: false
@@ -142,7 +218,7 @@ class TeamsList extends PureComponent {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
container: {
backgroundColor: theme.sidebarBg,
flex: 1
@@ -193,8 +269,66 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
moreAction: {
color: theme.sidebarHeaderTextColor,
fontSize: 30
},
teamWrapper: {
marginTop: 20
},
teamContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
marginHorizontal: 16
},
teamIconContainer: {
alignItems: 'center',
backgroundColor: theme.sidebarText,
borderRadius: 2,
height: 40,
justifyContent: 'center',
width: 40
},
teamIcon: {
color: theme.sidebarBg,
fontFamily: 'OpenSans',
fontSize: 18,
fontWeight: '600'
},
teamNameContainer: {
flex: 1,
flexDirection: 'column',
marginLeft: 10
},
teamName: {
color: theme.sidebarText,
fontSize: 18
},
teamUrl: {
color: changeOpacity(theme.sidebarText, 0.5),
fontSize: 12
},
checkmarkContainer: {
alignItems: 'flex-end'
},
checkmark: {
color: theme.sidebarText,
fontSize: 20
},
badge: {
backgroundColor: theme.mentionBj,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
flexDirection: 'row',
padding: 3,
position: 'absolute',
left: 45,
top: -7.5
},
mention: {
color: theme.mentionColor,
fontSize: 10
}
};
});
});
export default injectIntl(TeamsList);

View File

@@ -1,24 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import {removeProtocol} from 'app/utils/url';
import TeamsListItem from './teams_list_item.js';
function mapStateToProps(state, ownProps) {
return {
currentTeamId: getCurrentTeamId(state),
currentUrl: removeProtocol(getCurrentUrl(state)),
teamMember: getTeamMemberships(state)[ownProps.team.id],
theme: getTheme(state),
...ownProps
};
}
export default connect(mapStateToProps)(TeamsListItem);

View File

@@ -1,171 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {
Text,
TouchableHighlight,
View
} from 'react-native';
import IonIcon from 'react-native-vector-icons/Ionicons';
import Badge from 'app/components/badge';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class TeamsListItem extends React.PureComponent {
static propTypes = {
currentTeamId: PropTypes.string.isRequired,
currentUrl: PropTypes.string.isRequired,
selectTeam: PropTypes.func.isRequired,
team: PropTypes.object.isRequired,
teamMember: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
selectTeam = wrapWithPreventDoubleTap(() => {
this.props.selectTeam(this.props.team);
});
render() {
const {
currentTeamId,
currentUrl,
team,
teamMember,
theme
} = this.props;
const styles = getStyleSheet(theme);
let current;
let badge;
if (team.id === currentTeamId) {
current = (
<View style={styles.checkmarkContainer}>
<IonIcon
name='md-checkmark'
style={styles.checkmark}
/>
</View>
);
}
let badgeCount = 0;
if (teamMember.mention_count) {
badgeCount = teamMember.mention_count;
} else if (teamMember.msg_count) {
badgeCount = -1;
}
if (badgeCount) {
badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={badgeCount}
minHeight={20}
minWidth={20}
/>
);
}
return (
<View style={styles.teamWrapper}>
<TouchableHighlight
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={this.selectTeam}
>
<View style={styles.teamContainer}>
<View style={styles.teamIconContainer}>
<Text style={styles.teamIcon}>
{team.display_name.substr(0, 2).toUpperCase()}
</Text>
</View>
<View style={styles.teamNameContainer}>
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={styles.teamName}
>
{team.display_name}
</Text>
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={styles.teamUrl}
>
{`${currentUrl}/${team.name}`}
</Text>
</View>
{current}
</View>
</TouchableHighlight>
{badge}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
teamWrapper: {
marginTop: 20
},
teamContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
marginHorizontal: 16
},
teamIconContainer: {
alignItems: 'center',
backgroundColor: theme.sidebarText,
borderRadius: 2,
height: 40,
justifyContent: 'center',
width: 40
},
teamIcon: {
color: theme.sidebarBg,
fontFamily: 'OpenSans',
fontSize: 18,
fontWeight: '600'
},
teamNameContainer: {
flex: 1,
flexDirection: 'column',
marginLeft: 10
},
teamName: {
color: theme.sidebarText,
fontSize: 18
},
teamUrl: {
color: changeOpacity(theme.sidebarText, 0.5),
fontSize: 12
},
checkmarkContainer: {
alignItems: 'flex-end'
},
checkmark: {
color: theme.sidebarText,
fontSize: 20
},
badge: {
backgroundColor: theme.mentionBj,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
flexDirection: 'row',
padding: 3,
position: 'absolute',
left: 45,
top: -7.5
},
mention: {
color: theme.mentionColor,
fontSize: 10
}
};
});

View File

@@ -4,6 +4,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
Text,
View
} from 'react-native';
@@ -126,7 +127,7 @@ export default class ChannelIcon extends React.PureComponent {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
container: {
marginRight: 12,
alignItems: 'center'
@@ -173,5 +174,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
groupInfo: {
color: theme.centerChannelColor
}
};
});
});

View File

@@ -4,6 +4,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
Text,
TouchableOpacity,
View
@@ -71,7 +72,7 @@ class ChannelIntro extends PureComponent {
style={style.profile}
>
<ProfilePicture
userId={member.id}
user={member}
size={64}
statusBorderWidth={2}
statusSize={25}
@@ -300,6 +301,7 @@ class ChannelIntro extends PureComponent {
case General.PRIVATE_CHANNEL:
return this.buildPrivateChannelContent();
}
};
@@ -325,7 +327,7 @@ class ChannelIntro extends PureComponent {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
channelTitle: {
color: theme.centerChannelColor,
fontSize: 19,
@@ -366,7 +368,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flexWrap: 'wrap',
justifyContent: 'flex-start'
}
};
});
});
export default injectIntl(ChannelIntro);

View File

@@ -0,0 +1,106 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
View
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const GRADIENT_START = 0.05;
const GRADIENT_MIDDLE = 0.1;
const GRADIENT_END = 0.01;
function buildSections(key, style, theme, top) {
return (
<View
key={key}
style={[style.section, (top && {marginTop: -15})]}
>
<View style={style.avatar}/>
<View style={style.sectionMessage}>
<LinearGradient
start={{x: 0.0, y: 1.0}}
end={{x: 1.0, y: 1.0}}
colors={[
changeOpacity('#e5e5e4', GRADIENT_START),
changeOpacity('#d6d6d5', GRADIENT_MIDDLE),
changeOpacity('#e5e5e4', GRADIENT_END)
]}
locations={[0.1, 0.3, 0.7]}
style={[style.messageText, {width: 106}]}
/>
<LinearGradient
start={{x: 0.0, y: 1.0}}
end={{x: 1.0, y: 1.0}}
colors={[
changeOpacity('#e5e5e4', GRADIENT_START),
changeOpacity('#d6d6d5', GRADIENT_MIDDLE),
changeOpacity('#e5e5e4', GRADIENT_END)
]}
locations={[0.1, 0.3, 0.7]}
style={[style.messageText, {alignSelf: 'stretch'}]}
/>
<LinearGradient
start={{x: 0.0, y: 1.0}}
end={{x: 1.0, y: 1.0}}
colors={[
changeOpacity('#e5e5e4', GRADIENT_START),
changeOpacity('#d6d6d5', GRADIENT_MIDDLE),
changeOpacity('#e5e5e4', GRADIENT_END)
]}
locations={[0.1, 0.3, 0.7]}
style={[style.messageText, {alignSelf: 'stretch'}]}
/>
</View>
</View>
);
}
export default function channelLoader(props) {
const style = getStyleSheet(props.theme);
return (
<View style={style.container}>
{Array(10).fill().map((item, index) => buildSections(index, style, props.theme, index === 0))}
</View>
);
}
channelLoader.propTypes = {
theme: PropTypes.object.isRequired
};
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
avatar: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderRadius: 16,
height: 32,
width: 32
},
container: {
backgroundColor: theme.centerChannelBg,
flex: 1
},
messageText: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 10,
marginBottom: 10
},
section: {
flexDirection: 'row',
paddingLeft: 12,
paddingRight: 20,
marginVertical: 10
},
sectionMessage: {
marginLeft: 12,
flex: 1
}
});
});

View File

@@ -1,127 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
View
} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const GRADIENT_START = 0.05;
const GRADIENT_MIDDLE = 0.1;
const GRADIENT_END = 0.01;
export default class ChannelLoader extends PureComponent {
static propTypes = {
channelIsLoading: PropTypes.bool.isRequired,
deviceWidth: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired
};
buildSections(key, style, top) {
return (
<View
key={key}
style={[style.section, (top && {marginTop: Platform.OS === 'android' ? 0 : -15, paddingTop: 10})]}
>
<View style={style.avatar}/>
<View style={style.sectionMessage}>
<LinearGradient
start={{x: 0.0, y: 1.0}}
end={{x: 1.0, y: 1.0}}
colors={[
changeOpacity('#e5e5e4', GRADIENT_START),
changeOpacity('#d6d6d5', GRADIENT_MIDDLE),
changeOpacity('#e5e5e4', GRADIENT_END)
]}
locations={[0.1, 0.3, 0.7]}
style={[style.messageText, {width: 106}]}
/>
<LinearGradient
start={{x: 0.0, y: 1.0}}
end={{x: 1.0, y: 1.0}}
colors={[
changeOpacity('#e5e5e4', GRADIENT_START),
changeOpacity('#d6d6d5', GRADIENT_MIDDLE),
changeOpacity('#e5e5e4', GRADIENT_END)
]}
locations={[0.1, 0.3, 0.7]}
style={[style.messageText, {alignSelf: 'stretch'}]}
/>
<LinearGradient
start={{x: 0.0, y: 1.0}}
end={{x: 1.0, y: 1.0}}
colors={[
changeOpacity('#e5e5e4', GRADIENT_START),
changeOpacity('#d6d6d5', GRADIENT_MIDDLE),
changeOpacity('#e5e5e4', GRADIENT_END)
]}
locations={[0.1, 0.3, 0.7]}
style={[style.messageText, {alignSelf: 'stretch'}]}
/>
</View>
</View>
);
}
render() {
const {channelIsLoading, deviceWidth, theme} = this.props;
if (!channelIsLoading) {
return null;
}
const style = getStyleSheet(theme);
return (
<View style={[style.container, {width: deviceWidth}]}>
{Array(20).fill().map((item, index) => this.buildSections(index, style, index === 0))}
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
backgroundColor: theme.centerChannelBg,
flex: 1,
position: 'absolute',
...Platform.select({
android: {
top: 0
},
ios: {
top: 15
}
})
},
avatar: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderRadius: 16,
height: 32,
width: 32
},
messageText: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 10,
marginBottom: 10
},
section: {
backgroundColor: theme.centerChannelBg,
flexDirection: 'row',
paddingLeft: 12,
paddingRight: 20,
marginVertical: 10
},
sectionMessage: {
marginLeft: 12,
flex: 1
}
};
});

View File

@@ -1,19 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'app/selectors/preferences';
import ChannelLoader from './channel_loader';
function mapStateToProps(state, ownProps) {
const {deviceWidth} = state.device.dimension;
return {
...ownProps,
channelIsLoading: state.views.channel.loading,
deviceWidth,
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(ChannelLoader);

View File

@@ -4,6 +4,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
Text,
View
} from 'react-native';
@@ -67,7 +68,7 @@ export default class ChannelListRow extends React.PureComponent {
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
titleContainer: {
alignItems: 'center',
flexDirection: 'row'
@@ -90,5 +91,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
fontSize: 13,
color: changeOpacity(theme.centerChannelColor, 0.5)
}
};
});
});

View File

@@ -4,6 +4,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import {
StyleSheet,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
@@ -56,7 +57,7 @@ export default class CustomListRow extends React.PureComponent {
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
container: {
flexDirection: 'row',
height: 65,
@@ -90,5 +91,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
backgroundColor: '#378FD2',
borderWidth: 0
}
};
});
});

View File

@@ -2,7 +2,7 @@
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {ListView, Platform, Text, View} from 'react-native';
import {ListView, Platform, StyleSheet, Text, View} from 'react-native';
import Loading from 'app/components/loading';
import FormattedText from 'app/components/formatted_text';
@@ -239,7 +239,7 @@ export default class CustomList extends PureComponent {
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
listView: {
flex: 1,
backgroundColor: theme.centerChannelBg,
@@ -285,5 +285,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
fontSize: 26,
color: changeOpacity(theme.centerChannelColor, 0.5)
}
};
});
});

View File

@@ -4,6 +4,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
Text,
View
} from 'react-native';
@@ -40,7 +41,7 @@ export default class UserListRow extends React.PureComponent {
selected={this.props.selected}
>
<ProfilePicture
userId={this.props.user.id}
user={this.props.user}
size={32}
/>
<View style={style.textContainer}>
@@ -65,7 +66,7 @@ export default class UserListRow extends React.PureComponent {
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
container: {
flexDirection: 'row',
height: 65,
@@ -112,5 +113,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
backgroundColor: '#378FD2',
borderWidth: 0
}
};
});
});

View File

@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
import {
Platform,
SectionList,
StyleSheet,
Text,
View
} from 'react-native';
@@ -254,7 +255,7 @@ export default class CustomSectionList extends React.PureComponent {
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
listView: {
flex: 1,
backgroundColor: theme.centerChannelBg,
@@ -306,5 +307,5 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
fontSize: 26,
color: changeOpacity(theme.centerChannelColor, 0.5)
}
};
});
});

View File

@@ -12,7 +12,7 @@ export default class Drawer extends BaseDrawer {
};
// To fix the android onLayout issue give this a value of 100% as it does not need another one
getMainHeight = () => '100%';
getHeight = () => '100%';
processTapGestures = () => {
// Note that we explicitly don't support tap to open or double tap because I didn't copy them over

View File

@@ -3,161 +3,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Image, Platform, Text} from 'react-native';
import FastImage from 'react-native-fast-image';
import {Image, Text} from 'react-native';
import CustomPropTypes from 'app/constants/custom_prop_types';
import {EmojiIndicesByAlias, Emojis} from 'app/utils/emojis';
import {Client4} from 'mattermost-redux/client';
import {Client} from 'mattermost-redux/client';
export default class Emoji extends React.PureComponent {
static propTypes = {
customEmojis: PropTypes.object,
emojiName: PropTypes.string.isRequired,
fontSize: PropTypes.number,
literal: PropTypes.string,
padding: PropTypes.number,
size: PropTypes.number.isRequired,
textStyle: CustomPropTypes.Style,
token: PropTypes.string.isRequired
};
textStyle: CustomPropTypes.Style
}
static defaultProps = {
customEmojis: new Map(),
literal: ''
};
constructor(props) {
super(props);
this.state = {
...this.getImageUrl(props),
originalWidth: 0,
originalHeight: 0
};
}
componentWillMount() {
this.mounted = true;
if (this.state.imageUrl && this.state.isCustomEmoji) {
this.updateImageHeight(this.state.imageUrl);
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.customEmojis !== this.props.customEmojis || nextProps.emojiName !== this.props.emojiName) {
this.setState({
...this.getImageUrl(nextProps),
originalWidth: 0,
originalHeight: 0
});
}
}
componentWillUpdate(nextProps, nextState) {
if (nextState.imageUrl !== this.state.imageUrl && nextState.imageUrl && nextState.isCustomEmoji) {
this.updateImageHeight(nextState.imageUrl);
}
}
componentWillUnmount() {
this.mounted = false;
}
getImageUrl = (props = this.props) => {
const emojiName = props.emojiName;
let imageUrl = '';
let isCustomEmoji = false;
if (EmojiIndicesByAlias.has(emojiName)) {
const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)];
imageUrl = Client4.getSystemEmojiImageUrl(emoji.filename);
} else if (props.customEmojis.has(emojiName)) {
const emoji = props.customEmojis.get(emojiName);
imageUrl = Client4.getCustomEmojiImageUrl(emoji.id);
isCustomEmoji = true;
}
return {
imageUrl,
isCustomEmoji
};
}
updateImageHeight = (imageUrl) => {
Image.getSize(imageUrl, (originalWidth, originalHeight) => {
if (this.mounted) {
this.setState({
originalWidth,
originalHeight
});
}
});
literal: '',
padding: 10
}
render() {
const {
fontSize,
customEmojis,
emojiName,
literal,
padding,
size,
textStyle,
token
textStyle
} = this.props;
if (!this.state.imageUrl) {
let imageUrl;
if (EmojiIndicesByAlias.has(emojiName)) {
const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)];
imageUrl = Client.getSystemEmojiImageUrl(emoji.filename);
} else if (customEmojis.has(emojiName)) {
const emoji = customEmojis.get(emojiName);
imageUrl = Client.getCustomEmojiImageUrl(emoji.id);
}
if (!imageUrl) {
return <Text style={textStyle}>{literal}</Text>;
}
let ImageComponent;
if (Platform.OS === 'android') {
ImageComponent = Image;
} else {
ImageComponent = FastImage;
}
const source = {
uri: this.state.imageUrl,
headers: {
Authorization: `Bearer ${token}`
}
};
let width = size;
let height = size;
if (this.state.originalHeight && this.state.originalWidth) {
if (this.state.originalWidth > this.state.originalHeight) {
height = (size * this.state.originalHeight) / this.state.originalWidth;
} else if (this.state.originalWidth < this.state.originalHeight) {
// This may cause text to reflow, but its impossible to add a horizontal margin
width = (size * this.state.originalWidth) / this.state.originalHeight;
}
}
let marginTop = 0;
if (fontSize) {
// Center the image vertically on iOS (does nothing on Android)
marginTop = (height - fontSize) / 2;
// hack to get the vertical alignment looking better
if (fontSize === 17) {
marginTop -= 2;
} else if (fontSize === 15) {
marginTop += 1;
}
}
// Android can't change the size of an image after its first render, so
// force a new image to be rendered when the size changes
const key = Platform.OS === 'android' ? (height + '-' + width) : null;
return (
<ImageComponent
key={key}
style={{width, height, marginTop}}
source={source}
onError={this.onError}
<Image
style={{width: size, height: size, padding}}
source={{uri: imageUrl}}
/>
);
}

View File

@@ -9,9 +9,8 @@ import Emoji from './emoji';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
customEmojis: getCustomEmojisByName(state),
token: state.entities.general.credentials.token
...ownProps
};
}

View File

@@ -1,285 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
SectionList,
TouchableOpacity,
View
} from 'react-native';
import Emoji from 'app/components/emoji';
import FormattedText from 'app/components/formatted_text';
import SearchBar from 'app/components/search_bar';
import {emptyFunction} from 'app/utils/general';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
const EMOJI_SIZE = 30;
const EMOJI_GUTTER = 7.5;
const SECTION_MARGIN = 15;
class EmojiPicker extends PureComponent {
static propTypes = {
emojis: PropTypes.array.isRequired,
deviceWidth: PropTypes.number.isRequired,
intl: intlShape.isRequired,
onEmojiPress: PropTypes.func,
theme: PropTypes.object.isRequired
};
static defaultProps = {
onEmojiPress: emptyFunction
};
leftButton = {
id: 'close-edit-post'
};
constructor(props) {
super(props);
this.state = {
emojis: props.emojis,
searchTerm: '',
width: props.deviceWidth - (SECTION_MARGIN * 2)
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.deviceWidth !== this.props.deviceWidth) {
this.setState({
width: nextProps.deviceWidth - (SECTION_MARGIN * 2)
});
}
}
changeSearchTerm = (text) => {
this.setState({
searchTerm: text
});
clearTimeout(this.searchTermTimeout);
const timeout = text ? 350 : 0;
this.searchTermTimeout = setTimeout(() => {
const emojis = this.searchEmojis(text);
this.setState({
emojis
});
}, timeout);
};
cancelSearch = () => {
this.setState({
emojis: this.props.emojis,
searchTerm: ''
});
};
filterEmojiAliases = (aliases, searchTerm) => {
return aliases.findIndex((alias) => alias.includes(searchTerm)) !== -1;
};
searchEmojis = (searchTerm) => {
const {emojis} = this.props;
const searchTermLowerCase = searchTerm.toLowerCase();
if (!searchTerm) {
return emojis;
}
const nextEmojis = [];
emojis.forEach((section) => {
const {data, ...otherProps} = section;
const {key, items} = data[0];
const nextData = {
key,
items: items.filter((item) => {
if (item.aliases) {
return this.filterEmojiAliases(item.aliases, searchTermLowerCase);
}
return item.name.includes(searchTermLowerCase);
})
};
if (nextData.items.length) {
nextEmojis.push({
...otherProps,
data: [nextData]
});
}
});
return nextEmojis;
};
renderSectionHeader = ({section}) => {
const {theme} = this.props;
const styles = getStyleSheetFromTheme(theme);
return (
<View key={section.title}>
<FormattedText
style={styles.sectionTitle}
id={section.id}
defaultMessage={section.defaultMessage}
/>
</View>
);
};
renderEmojis = (emojis, index) => {
const {theme} = this.props;
const styles = getStyleSheetFromTheme(theme);
return (
<View
key={index}
style={styles.columnStyle}
>
{emojis.map((emoji, emojiIndex) => {
const style = [styles.emoji];
if (emojiIndex === 0) {
style.push(styles.emojiLeft);
} else if (emojiIndex === emojis.length - 1) {
style.push(styles.emojiRight);
}
return (
<TouchableOpacity
key={emoji.name}
style={style}
onPress={() => {
this.props.onEmojiPress(emoji.name);
}}
>
<Emoji
emojiName={emoji.name}
size={EMOJI_SIZE}
/>
</TouchableOpacity>
);
})}
</View>
);
};
renderItem = ({item}) => {
const {theme} = this.props;
const styles = getStyleSheetFromTheme(theme);
const numColumns = Number((this.state.width / (EMOJI_SIZE + (EMOJI_GUTTER * 2))).toFixed(0));
const slices = item.items.reduce((slice, emoji, emojiIndex) => {
if (emojiIndex % numColumns === 0 && emojiIndex !== 0) {
slice.push([]);
}
slice[slice.length - 1].push(emoji);
return slice;
}, [[]]);
return (
<View style={styles.section}>
{slices.map(this.renderEmojis)}
</View>
);
};
render() {
const {intl, theme} = this.props;
const {emojis, searchTerm} = this.state;
const {formatMessage} = intl;
const styles = getStyleSheetFromTheme(theme);
return (
<View style={styles.wrapper}>
<View style={styles.searchBar}>
<SearchBar
ref='search_bar'
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
cancelTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={{
backgroundColor: theme.centerChannelBg,
color: theme.centerChannelColor,
fontSize: 13
}}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
tintColorSearch={changeOpacity(theme.centerChannelColor, 0.8)}
tintColorDelete={changeOpacity(theme.centerChannelColor, 0.5)}
titleCancelColor={theme.centerChannelColor}
onChangeText={this.changeSearchTerm}
onCancelButtonPress={this.cancelSearch}
value={searchTerm}
/>
</View>
<View style={styles.container}>
<SectionList
showsVerticalScrollIndicator={false}
style={styles.listView}
sections={emojis}
renderSectionHeader={this.renderSectionHeader}
renderItem={this.renderItem}
removeClippedSubviews={true}
/>
</View>
</View>
);
}
}
const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
return {
columnStyle: {
alignSelf: 'stretch',
flexDirection: 'row',
marginVertical: EMOJI_GUTTER,
justifyContent: 'flex-start'
},
container: {
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
flex: 1
},
emoji: {
width: EMOJI_SIZE,
height: EMOJI_SIZE,
marginHorizontal: EMOJI_GUTTER,
alignItems: 'center',
justifyContent: 'center'
},
emojiLeft: {
marginLeft: 0
},
emojiRight: {
marginRight: 0
},
listView: {
backgroundColor: theme.centerChannelBg
},
searchBar: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
paddingVertical: 5
},
section: {
alignItems: 'center'
},
sectionTitle: {
color: changeOpacity(theme.centerChannelColor, 0.2),
fontSize: 15,
fontWeight: '700',
paddingVertical: 5
},
wrapper: {
flex: 1
}
};
});
export default injectIntl(EmojiPicker);

View File

@@ -1,111 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {createSelector} from 'reselect';
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
import {getDimensions} from 'app/selectors/device';
import {getTheme} from 'app/selectors/preferences';
import {CategoryNames, Emojis, EmojiIndicesByCategory} from 'app/utils/emojis';
import EmojiPicker from './emoji_picker';
const categoryToI18n = {
activity: {
id: 'mobile.emoji_picker.activity',
defaultMessage: 'ACTIVITY'
},
custom: {
id: 'mobile.emoji_picker.custom',
defaultMessage: 'CUSTOM'
},
flags: {
id: 'mobile.emoji_picker.flags',
defaultMessage: 'FLAGS'
},
foods: {
id: 'mobile.emoji_picker.foods',
defaultMessage: 'FOODS'
},
nature: {
id: 'mobile.emoji_picker.nature',
defaultMessage: 'NATURE'
},
objects: {
id: 'mobile.emoji_picker.objects',
defaultMessage: 'OBJECTS'
},
people: {
id: 'mobile.emoji_picker.people',
defaultMessage: 'PEOPLE'
},
places: {
id: 'mobile.emoji_picker.places',
defaultMessage: 'PLACES'
},
symbols: {
id: 'mobile.emoji_picker.symbols',
defaultMessage: 'SYMBOLS'
}
};
function fillEmoji(indice) {
const emoji = Emojis[indice];
return {
name: emoji.aliases[0],
aliases: emoji.aliases
};
}
const getEmojisBySection = createSelector(
getCustomEmojisByName,
(customEmojis) => {
const emoticons = CategoryNames.filter((name) => name !== 'custom').map((category) => {
const section = {
...categoryToI18n[category],
key: category,
data: [{
key: `${category}-emojis`,
items: EmojiIndicesByCategory.get(category).map(fillEmoji)
}]
};
return section;
});
const customEmojiData = {
key: 'custom-emojis',
title: 'CUSTOM',
items: []
};
for (const [key] of customEmojis) {
customEmojiData.items.push({
name: key
});
}
emoticons.push({
...categoryToI18n.custom,
key: 'custom',
data: [customEmojiData]
});
return emoticons;
}
);
function mapStateToProps(state) {
const emojis = getEmojisBySection(state);
const {deviceWidth} = getDimensions(state);
return {
emojis,
deviceWidth,
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(EmojiPicker);

View File

@@ -4,7 +4,7 @@
import {connect} from 'react-redux';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Text} from 'react-native';
import {StyleSheet, Text} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import {getTheme} from 'app/selectors/preferences';
@@ -14,7 +14,12 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
class ErrorText extends PureComponent {
static propTypes = {
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
theme: PropTypes.object.isRequired
theme: PropTypes.object
};
static defaultProps = {
error: {},
theme: {}
};
render() {
@@ -46,11 +51,11 @@ class ErrorText extends PureComponent {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
errorLabel: {
color: (theme.errorTextColor || '#DA4A4A')
}
};
});
});
function mapStateToProps(state, ownProps) {

View File

@@ -6,7 +6,8 @@ import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity,
View
View,
StyleSheet
} from 'react-native';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -64,7 +65,7 @@ export default class FileAttachment extends PureComponent {
const style = getStyleSheet(theme);
let fileAttachmentComponent;
if (file.has_preview_image || file.loading || file.mime_type === 'image/gif') {
if (file.has_preview_image || file.loading) {
fileAttachmentComponent = (
<FileAttachmentImage
addFileToFetchCache={this.props.addFileToFetchCache}
@@ -99,7 +100,7 @@ export default class FileAttachment extends PureComponent {
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
return StyleSheet.create({
downloadIcon: {
color: changeOpacity(theme.centerChannelColor, 0.7),
marginRight: 5
@@ -134,5 +135,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2)
}
};
});
});

View File

@@ -11,7 +11,7 @@ import {
StyleSheet
} from 'react-native';
import {Client4} from 'mattermost-redux/client';
import {Client} from 'mattermost-redux/client';
import imageIcon from 'assets/images/icons/image.png';
@@ -105,12 +105,12 @@ export default class FileAttachmentImage extends PureComponent {
switch (imageSize) {
case IMAGE_SIZE.Fullsize:
return Client4.getFileUrl(file.id, this.state.timestamp);
return Client.getFileUrl(file.id, this.state.timestamp);
case IMAGE_SIZE.Preview:
return Client4.getFilePreviewUrl(file.id, this.state.timestamp);
return Client.getFilePreviewUrl(file.id, this.state.timestamp);
case IMAGE_SIZE.Thumbnail:
default:
return Client4.getFileThumbnailUrl(file.id, this.state.timestamp);
return Client.getFileThumbnailUrl(file.id, this.state.timestamp);
}
};
@@ -123,7 +123,7 @@ export default class FileAttachmentImage extends PureComponent {
}
return newWidth;
};
}
render() {
const {

View File

@@ -4,6 +4,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Dimensions,
Platform,
ScrollView,
StyleSheet,
@@ -16,6 +17,8 @@ import FileAttachmentImage from 'app/components/file_attachment_list/file_attach
import FileAttachmentIcon from 'app/components/file_attachment_list/file_attachment_icon';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
const {height: deviceHeight} = Dimensions.get('window');
export default class FileUploadPreview extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
@@ -26,7 +29,6 @@ export default class FileUploadPreview extends PureComponent {
channelId: PropTypes.string.isRequired,
channelIsLoading: PropTypes.bool,
createPostRequestStatus: PropTypes.string.isRequired,
deviceHeight: PropTypes.number.isRequired,
fetchCache: PropTypes.object.isRequired,
files: PropTypes.array.isRequired,
inputHeight: PropTypes.number.isRequired,
@@ -96,7 +98,7 @@ export default class FileUploadPreview extends PureComponent {
</View>
);
});
};
}
render() {
if (this.props.channelIsLoading || (!this.props.files.length && !this.props.filesUploadingForCurrentChannel)) {
@@ -105,7 +107,7 @@ export default class FileUploadPreview extends PureComponent {
return (
<KeyboardLayout>
<View style={[style.container, {height: this.props.deviceHeight}]}>
<View style={[style.container]}>
<ScrollView
horizontal={true}
style={style.scrollView}
@@ -122,6 +124,7 @@ export default class FileUploadPreview extends PureComponent {
const style = StyleSheet.create({
container: {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
height: deviceHeight,
left: 0,
bottom: 0,
position: 'absolute',

View File

@@ -7,7 +7,6 @@ import {createSelector} from 'reselect';
import {handleRemoveFile, retryFileUpload} from 'app/actions/views/file_upload';
import {addFileToFetchCache} from 'app/actions/views/file_preview';
import {getDimensions} from 'app/selectors/device';
import {getTheme} from 'app/selectors/preferences';
import FileUploadPreview from './file_upload_preview';
@@ -21,22 +20,15 @@ const checkForFileUploadingInChannel = createSelector(
return state.views.channel.drafts[channelId];
},
(draft) => {
if (!draft || !draft.files) {
return false;
}
return draft.files.some((f) => f.loading);
}
);
function mapStateToProps(state, ownProps) {
const {deviceHeight} = getDimensions(state);
return {
...ownProps,
channelIsLoading: state.views.channel.loading,
createPostRequestStatus: state.requests.posts.createPost.status,
deviceHeight,
fetchCache: state.views.fetchCache,
filesUploadingForCurrentChannel: checkForFileUploadingInChannel(state, ownProps.channelId, ownProps.rootId),
theme: getTheme(state)

View File

@@ -1,12 +1,12 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {Text} from 'react-native';
class FormattedDate extends React.PureComponent {
class FormattedDate extends Component {
static propTypes = {
intl: intlShape.isRequired,
value: PropTypes.any.isRequired,

View File

@@ -1,12 +1,12 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {createElement, isValidElement} from 'react';
import {Component, createElement, isValidElement} from 'react';
import PropTypes from 'prop-types';
import {Text} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
class FormattedText extends React.PureComponent {
class FormattedText extends Component {
static propTypes = {
intl: intlShape.isRequired,
id: PropTypes.string.isRequired,
@@ -80,9 +80,9 @@ class FormattedText extends React.PureComponent {
// approach allows messages to render with React Elements while
// keeping React's virtual diffing working properly.
nodes = formattedMessage.
split(tokenDelimiter).
filter((part) => Boolean(part)).
map((part) => elements[part] || part);
split(tokenDelimiter).
filter((part) => Boolean(part)).
map((part) => elements[part] || part);
} else {
nodes = [formattedMessage];
}

View File

@@ -1,12 +1,12 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {Text} from 'react-native';
class FormattedTime extends React.PureComponent {
class FormattedTime extends Component {
static propTypes = {
intl: intlShape.isRequired,
value: PropTypes.any.isRequired,

View File

@@ -1,11 +1,11 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {KeyboardAvoidingView, Platform, View} from 'react-native';
export default class KeyboardLayout extends React.PureComponent {
export default class KeyboardLayout extends Component {
static propTypes = {
behaviour: PropTypes.string,
children: PropTypes.node,

View File

@@ -29,7 +29,6 @@ export default class Markdown extends PureComponent {
baseTextStyle: CustomPropTypes.Style,
blockStyles: PropTypes.object,
emojiSizes: PropTypes.object,
fontSizes: PropTypes.object,
isSearchResult: PropTypes.bool,
navigator: PropTypes.object.isRequired,
onLongPress: PropTypes.func,
@@ -53,25 +52,16 @@ export default class Markdown extends PureComponent {
text: 20
},
android: {
heading1: 60,
heading2: 60,
heading3: 60,
heading4: 60,
heading5: 60,
heading6: 60,
text: 45
heading1: 80,
heading2: 80,
heading3: 80,
heading4: 80,
heading5: 80,
heading6: 80,
text: 65
}
})
},
fontSizes: {
heading1: 17,
heading2: 17,
heading3: 17,
heading4: 17,
heading5: 17,
heading6: 17,
text: 15
},
onLongPress: () => true
};
@@ -89,7 +79,6 @@ export default class Markdown extends PureComponent {
emph: Renderer.forwardChildren,
strong: Renderer.forwardChildren,
del: Renderer.forwardChildren,
code: this.renderCodeSpan,
link: this.renderLink,
image: this.renderImage,
@@ -167,14 +156,11 @@ export default class Markdown extends PureComponent {
renderEmoji = ({context, emojiName, literal}) => {
let size;
let fontSize;
const headingType = context.find((type) => type.startsWith('heading'));
if (headingType) {
size = this.props.emojiSizes[headingType];
fontSize = this.props.fontSizes[headingType];
} else {
size = this.props.emojiSizes.text;
fontSize = this.props.fontSizes.text;
}
return (
@@ -182,7 +168,6 @@ export default class Markdown extends PureComponent {
emojiName={emojiName}
literal={literal}
size={size}
fontSize={fontSize}
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
/>
);
@@ -215,15 +200,15 @@ export default class Markdown extends PureComponent {
renderCodeBlock = (props) => {
// These sometimes include a trailing newline
const content = props.literal.replace(/\n$/, '');
const contents = props.literal.replace(/\n$/, '');
return (
<MarkdownCodeBlock
navigator={this.props.navigator}
content={content}
language={props.language}
textStyle={this.props.textStyles.codeBlock}
/>
blockStyle={this.props.blockStyles.codeBlock}
textStyle={concatStyles(this.props.baseTextStyle, this.props.textStyles.codeBlock)}
>
{contents}
</MarkdownCodeBlock>
);
}
@@ -250,13 +235,10 @@ export default class Markdown extends PureComponent {
);
}
renderListItem = ({children, context, ...otherProps}) => {
const level = context.filter((type) => type === 'list').length;
renderListItem = ({children, ...otherProps}) => {
return (
<MarkdownListItem
bulletStyle={this.props.baseTextStyle}
level={level}
{...otherProps}
>
{children}

View File

@@ -0,0 +1,28 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import {ScrollView, Text} from 'react-native';
import CustomPropTypes from 'app/constants/custom_prop_types';
export default class MarkdownCodeBlock extends PureComponent {
static propTypes = {
children: CustomPropTypes.Children,
blockStyle: CustomPropTypes.Style,
textStyle: CustomPropTypes.Style
};
render() {
return (
<ScrollView
style={this.props.blockStyle}
horizontal={true}
>
<Text style={this.props.textStyle}>
{this.props.children}
</Text>
</ScrollView>
);
}
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'app/selectors/preferences';
import MarkdownCodeBlock from './markdown_code_block';
function mapStateToProps(state, ownProps) {
return {
theme: getTheme(state),
...ownProps
};
}
export default connect(mapStateToProps)(MarkdownCodeBlock);

View File

@@ -1,216 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {PropTypes} from 'prop-types';
import React from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {
StyleSheet,
Text,
TouchableOpacity,
View
} from 'react-native';
import CustomPropTypes from 'app/constants/custom_prop_types';
import FormattedText from 'app/components/formatted_text';
import {getDisplayNameForLanguage} from 'app/utils/markdown';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const MAX_LINES = 4;
class MarkdownCodeBlock extends React.PureComponent {
static propTypes = {
intl: intlShape.isRequired,
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
language: PropTypes.string,
content: PropTypes.string.isRequired,
textStyle: CustomPropTypes.Style
};
static defaultProps = {
language: ''
};
handlePress = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
const languageDisplayName = getDisplayNameForLanguage(this.props.language);
let title;
if (languageDisplayName) {
title = intl.formatMessage(
{
id: 'mobile.routes.code',
defaultMessage: '{language} Code'
},
{
language: languageDisplayName
}
);
} else {
title = intl.formatMessage({
id: 'mobile.routes.code.noLanguage',
defaultMessage: 'Code'
});
}
navigator.push({
screen: 'Code',
title,
animated: true,
backButtonTitle: '',
passProps: {
content: this.props.content
},
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
}
});
});
trimContent = (content) => {
const lines = content.split('\n');
const numberOfLines = lines.length;
if (numberOfLines > MAX_LINES) {
return {
content: lines.slice(0, MAX_LINES).join('\n'),
numberOfLines
};
}
return {
content,
numberOfLines
};
};
render() {
const style = getStyleSheet(this.props.theme);
let language = null;
if (this.props.language) {
const languageDisplayName = getDisplayNameForLanguage(this.props.language);
if (languageDisplayName) {
language = (
<View style={style.language}>
<Text style={style.languageText}>
{languageDisplayName}
</Text>
</View>
);
}
}
const {content, numberOfLines} = this.trimContent(this.props.content);
let lineNumbers = '1';
for (let i = 1; i < Math.min(numberOfLines, MAX_LINES); i++) {
const line = (i + 1).toString();
lineNumbers += '\n' + line;
}
let plusMoreLines = null;
if (numberOfLines > MAX_LINES) {
plusMoreLines = (
<FormattedText
style={style.plusMoreLinesText}
id='mobile.markdown.code.plusMoreLines'
defaultMessage='+{count, number} more lines'
values={{
count: numberOfLines - MAX_LINES
}}
/>
);
}
return (
<TouchableOpacity onPress={this.handlePress}>
<View style={style.container}>
<View style={style.lineNumbers}>
<Text style={style.lineNumbersText}>
{lineNumbers}
</Text>
</View>
<View style={style.rightColumn}>
<View style={style.code}>
<Text style={[style.codeText, this.props.textStyle]}>
{content}
</Text>
</View>
{plusMoreLines}
</View>
{language}
</View>
</TouchableOpacity>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
borderColor: changeOpacity(theme.centerChannelColor, 0.15),
borderRadius: 3,
borderWidth: StyleSheet.hairlineWidth,
flexDirection: 'row'
},
lineNumbers: {
alignItems: 'center',
backgroundColor: changeOpacity(theme.centerChannelColor, 0.05),
borderRightColor: changeOpacity(theme.centerChannelColor, 0.15),
borderRightWidth: StyleSheet.hairlineWidth,
flexDirection: 'column',
justifyContent: 'flex-start',
paddingVertical: 4,
width: 21
},
lineNumbersText: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 12,
lineHeight: 18
},
rightColumn: {
flexDirection: 'column',
flex: 1,
paddingHorizontal: 6,
paddingVertical: 4
},
code: {
flexDirection: 'row',
overflow: 'scroll' // Doesn't actually cause a scrollbar, but stops text from wrapping
},
codeText: {
color: changeOpacity(theme.centerChannelColor, 0.65),
fontSize: 12,
lineHeight: 18
},
plusMoreLinesText: {
color: changeOpacity(theme.centerChannelColor, 0.4),
fontSize: 11,
marginTop: 2
},
language: {
alignItems: 'center',
backgroundColor: theme.sidebarHeaderBg,
justifyContent: 'center',
opacity: 0.8,
padding: 6,
position: 'absolute',
right: 0,
top: 0
},
languageText: {
color: theme.sidebarHeaderTextColor,
fontSize: 12
}
};
});
export default injectIntl(MarkdownCodeBlock);

View File

@@ -18,8 +18,7 @@ export default class MarkdownListItem extends PureComponent {
startAt: PropTypes.number,
index: PropTypes.number.isRequired,
tight: PropTypes.bool,
bulletStyle: CustomPropTypes.Style,
level: PropTypes.number
bulletStyle: CustomPropTypes.Style
};
static defaultProps = {
@@ -30,15 +29,13 @@ export default class MarkdownListItem extends PureComponent {
let bullet;
if (this.props.ordered) {
bullet = (this.props.startAt + this.props.index) + '. ';
} else if (this.props.level % 2 === 0) {
bullet = '◦';
} else {
bullet = '•';
bullet = '• ';
}
return (
<View style={style.container}>
<View style={style.bullet}>
<View>
<Text style={this.props.bulletStyle}>
{bullet}
</Text>
@@ -52,9 +49,6 @@ export default class MarkdownListItem extends PureComponent {
}
const style = StyleSheet.create({
bullet: {
width: 15
},
container: {
flexDirection: 'row',
alignItems: 'flex-start'

View File

@@ -6,21 +6,17 @@ import {connect} from 'react-redux';
import {close as closeWebSocket, init as initWebSocket} from 'mattermost-redux/actions/websocket';
import {getConnection} from 'app/selectors/device';
import OfflineIndicator from './offline_indicator';
function mapStateToProps(state, ownProps) {
const {websocket} = state.requests.general;
const {appState} = state.entities.general;
const webSocketStatus = websocket.status;
const isConnecting = websocket.error >= 2;
const {connection} = state.views;
return {
appState,
isConnecting,
isOnline: getConnection(state),
webSocketStatus,
isOnline: connection,
websocket,
...ownProps
};
}

View File

@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
import {
ActivityIndicator,
Animated,
Dimensions,
Platform,
StyleSheet,
TouchableOpacity,
@@ -31,9 +32,8 @@ export default class OfflineIndicator extends PureComponent {
initWebSocket: PropTypes.func.isRequired
}).isRequired,
appState: PropTypes.bool,
isConnecting: PropTypes.bool,
isOnline: PropTypes.bool,
webSocketStatus: PropTypes.string
websocket: PropTypes.object
};
static defaultProps: {
@@ -45,6 +45,7 @@ export default class OfflineIndicator extends PureComponent {
super(props);
this.state = {
forced: false,
network: null,
top: new Animated.Value(INITIAL_TOP)
};
@@ -53,15 +54,12 @@ export default class OfflineIndicator extends PureComponent {
}
componentWillReceiveProps(nextProps) {
const {appState, isConnecting, isOnline, webSocketStatus} = nextProps;
if (appState) { // The app is in the foreground
if (isOnline) {
if (this.state.network && webSocketStatus === RequestStatus.SUCCESS) {
// Show the connected animation only if we had a previous network status
this.connected();
} else if ((webSocketStatus === RequestStatus.STARTED || webSocketStatus === RequestStatus.FAILURE) && isConnecting) {
// Show the connecting bar if it failed to connect at least twice
if (this.props.appState || nextProps.appState) {
if (this.state.forced || nextProps.isOnline) {
if (nextProps.websocket.status === RequestStatus.STARTED || nextProps.websocket.status === RequestStatus.FAILURE) {
this.connecting();
} else if (nextProps.websocket.status === RequestStatus.SUCCESS) {
this.connected();
}
} else {
this.offline();
@@ -76,17 +74,16 @@ export default class OfflineIndicator extends PureComponent {
};
connect = () => {
const {actions, isOnline, webSocketStatus} = this.props;
const {closeWebSocket, initWebSocket} = actions;
initWebSocket(Platform.OS);
const {closeWebSocket, initWebSocket} = this.props.actions;
this.setState({forced: true}, () => {
initWebSocket(Platform.OS);
// close the WS connection after trying for 5 seconds
setTimeout(() => {
if (webSocketStatus !== RequestStatus.SUCCESS) {
// set forced to be false after trying for 3 seconds
setTimeout(() => {
closeWebSocket(true);
this.setState({network: isOnline ? OFFLINE : CONNECTING});
}
}, 5000);
this.setState({forced: false, network: OFFLINE});
}, 3000);
});
};
connecting = () => {
@@ -116,7 +113,7 @@ export default class OfflineIndicator extends PureComponent {
)
]).start(() => {
this.backgroundColor.setValue(0);
this.setState({network: null});
this.setState({forced: false});
});
};
@@ -187,8 +184,8 @@ export default class OfflineIndicator extends PureComponent {
}
return (
<Animated.View style={[styles.container, {top: this.state.top, backgroundColor: background}]}>
<Animated.View style={styles.wrapper}>
<Animated.View style={[styles.container, {top: this.state.top}]}>
<Animated.View style={[styles.wrapper, {backgroundColor: background}]}>
<FormattedText
defaultMessage={defaultMessage}
id={i18nId}
@@ -204,7 +201,7 @@ export default class OfflineIndicator extends PureComponent {
const styles = StyleSheet.create({
container: {
height: HEIGHT,
width: '100%',
width: Dimensions.get('window').width,
zIndex: 9,
position: 'absolute'
},
@@ -214,7 +211,8 @@ const styles = StyleSheet.create({
height: HEIGHT,
flexDirection: 'row',
paddingLeft: 12,
paddingRight: 5
paddingRight: 5,
backgroundColor: 'red'
},
message: {
color: '#FFFFFF',

View File

@@ -39,11 +39,11 @@ export default class OptionsContext extends PureComponent {
};
handleHideUnderlay = () => {
this.props.toggleSelected(false, this.props.actions.length > 0);
this.props.toggleSelected(false);
};
handleShowUnderlay = () => {
this.props.toggleSelected(true, this.props.actions.length > 0);
this.props.toggleSelected(true);
};
render() {

View File

@@ -17,24 +17,12 @@ export default class OptionsContext extends PureComponent {
actions: []
};
handleHideUnderlay = () => {
if (!this.isShowing) {
this.props.toggleSelected(false, false);
}
};
handleShowUnderlay = () => {
this.props.toggleSelected(true, false);
};
handleHide = () => {
this.isShowing = false;
this.props.toggleSelected(false, this.props.actions.length > 0);
this.props.toggleSelected(false);
};
handleShow = () => {
this.isShowing = this.props.actions.length > 0;
this.props.toggleSelected(true, this.isShowing);
this.props.toggleSelected(true);
};
hide = () => {
@@ -45,21 +33,14 @@ export default class OptionsContext extends PureComponent {
this.refs.toolTip.showMenu();
};
handlePress = () => {
this.props.toggleSelected(false, this.props.actions.length > 0);
this.props.onPress();
};
render() {
return (
<ToolTip
onHideUnderlay={this.handleHideUnderlay}
onShowUnderlay={this.handleShowUnderlay}
ref='toolTip'
actions={this.props.actions}
arrowDirection='down'
longPress={true}
onPress={this.handlePress}
onPress={this.props.onPress}
underlayColor='transparent'
onShow={this.handleShow}
onHide={this.handleHide}

Some files were not shown because too many files have changed in this diff Show More