forked from Ivasoft/mattermost-mobile
Compare commits
11 Commits
release-1.
...
v1.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06aa01507f | ||
|
|
60bf695789 | ||
|
|
24eaef38ef | ||
|
|
53ae78674e | ||
|
|
d7ef19f883 | ||
|
|
15e05c44f5 | ||
|
|
12f6b11e09 | ||
|
|
10fd389e15 | ||
|
|
e50049d0d6 | ||
|
|
9d3e072f7a | ||
|
|
537598cc64 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -65,11 +65,6 @@ mattermost.keystore
|
||||
|
||||
# Sentry
|
||||
android/sentry.properties
|
||||
ios/sentry.properties
|
||||
|
||||
# Testing
|
||||
.nyc_output
|
||||
|
||||
# Pods
|
||||
.podinstall
|
||||
ios/Pods/
|
||||
|
||||
65
CHANGELOG.md
65
CHANGELOG.md
@@ -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
|
||||
|
||||
|
||||
@@ -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
148
Makefile
@@ -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:
|
||||
@:
|
||||
|
||||
|
||||
355
NOTICE.txt
355
NOTICE.txt
@@ -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.
|
||||
|
||||
19
README.md
19
README.md
@@ -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 there’s 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).
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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 it’s set to background to protect any confidential on-screen information, it also prevents taking screenshots of the app.</string>
|
||||
<string name="jailbreakProtection_title">Jailbreak / Root Detection</string>
|
||||
<string name="jailbreakProtection_description">Disable app launch on Jailbroken or rooted devices.</string>
|
||||
<string name="copyAndPasteProtection_title">Copy&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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
13
app/actions/views/connection.js
Normal file
13
app/actions/views/connection.js
Normal 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);
|
||||
};
|
||||
}
|
||||
@@ -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));
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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()}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
53
app/components/channel_drawer/swiper/swiper.android.js
Normal file
53
app/components/channel_drawer/swiper/swiper.android.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
});
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
106
app/components/channel_loader.js
Normal file
106
app/components/channel_loader.js
Normal 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
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,8 @@ import Emoji from './emoji';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
customEmojis: getCustomEmojisByName(state),
|
||||
token: state.entities.general.credentials.token
|
||||
...ownProps
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
28
app/components/markdown/markdown_code_block.js
Normal file
28
app/components/markdown/markdown_code_block.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user