forked from Ivasoft/mattermost-mobile
Compare commits
7 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc179a6516 | ||
|
|
fea6372819 | ||
|
|
87e89de854 | ||
|
|
534af426c9 | ||
|
|
553f3796b1 | ||
|
|
18b3d6eec9 | ||
|
|
73c81bb863 |
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:
|
||||
@:
|
||||
|
||||
|
||||
199
NOTICE.txt
199
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.
|
||||
@@ -1246,169 +1279,3 @@ 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.
|
||||
|
||||
11
README.md
11
README.md
@@ -2,9 +2,6 @@
|
||||
|
||||
**Supported Server Versions:** 4.0+
|
||||
|
||||
**Supported iOS versions:** 9.3+
|
||||
**Supported Android versions:** 5.0+
|
||||
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 11 languages. Learn more at https://mattermost.com.
|
||||
|
||||
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or package them yourself.
|
||||
@@ -148,11 +145,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 49
|
||||
versionName "1.2.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 ->
|
||||
@@ -170,17 +162,6 @@ dependencies {
|
||||
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" />
|
||||
@@ -43,19 +42,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");
|
||||
|
||||
|
||||
@@ -6,10 +6,6 @@ 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;
|
||||
@@ -62,11 +58,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
new RNNotificationsPackage(this),
|
||||
new LocalAuthPackage(),
|
||||
new JailMonkeyPackage(),
|
||||
new RNFetchBlobPackage(),
|
||||
new MattermostPackage(this),
|
||||
new RNSentryPackage(this),
|
||||
new ReactNativeExceptionHandlerPackage(),
|
||||
new ReactNativeYouTube()
|
||||
new MattermostManagedPackage()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,19 +10,15 @@ 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;
|
||||
public class MattermostManagedPackage implements ReactPackage {
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(MattermostManagedModule.getInstance(reactContext));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(
|
||||
MattermostManagedModule.getInstance(reactContext),
|
||||
NotificationPreferencesModule.getInstance(mApplication, reactContext)
|
||||
);
|
||||
public List<Class<? extends JavaScriptModule>> createJSModules() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -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,7 +1,6 @@
|
||||
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;
|
||||
@@ -11,7 +10,6 @@ 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;
|
||||
@@ -145,15 +143,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;
|
||||
|
||||
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,12 +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'
|
||||
|
||||
@@ -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;
|
||||
@@ -218,35 +182,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 +218,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 +275,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 +290,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 +300,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 +350,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,13 +4,15 @@
|
||||
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 {markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
|
||||
|
||||
export function loadConfigAndLicense() {
|
||||
return async (dispatch, getState) => {
|
||||
@@ -40,34 +42,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 +84,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,47 @@
|
||||
// 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,
|
||||
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 +50,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 +149,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,63 +158,141 @@ 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -222,11 +300,72 @@ export default class AtMention extends PureComponent {
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
listView: {
|
||||
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,37 @@
|
||||
// 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,
|
||||
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 +42,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 +140,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,54 +149,88 @@ 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -186,11 +238,57 @@ export default class ChannelMention extends PureComponent {
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
listView: {
|
||||
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);
|
||||
@@ -13,19 +13,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 +47,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 +70,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 +99,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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
@@ -26,9 +26,9 @@ export default class ChannelItem extends PureComponent {
|
||||
|
||||
onPress = () => {
|
||||
const {channel, onSelectChannel} = this.props;
|
||||
requestAnimationFrame(() => {
|
||||
setTimeout(() => {
|
||||
preventDoubleTap(onSelectChannel, this, channel);
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -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,406 @@
|
||||
// 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,
|
||||
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 {
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import NotificationPreferences from './notification_preferences';
|
||||
export default NotificationPreferences;
|
||||
// Used to leverage the platform specific components
|
||||
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,6 +4,7 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
InteractionManager,
|
||||
FlatList,
|
||||
Platform,
|
||||
Text,
|
||||
@@ -11,24 +12,28 @@ import {
|
||||
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
|
||||
@@ -43,17 +48,21 @@ class TeamsList extends PureComponent {
|
||||
}
|
||||
|
||||
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 +88,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 +177,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 +204,7 @@ class TeamsList extends PureComponent {
|
||||
<FlatList
|
||||
data={teams}
|
||||
renderItem={this.renderItem}
|
||||
keyExtractor={this.keyExtractor}
|
||||
keyExtractor={(item) => item.id}
|
||||
viewabilityConfig={{
|
||||
viewAreaCoveragePercentThreshold: 3,
|
||||
waitForInteraction: false
|
||||
@@ -193,6 +267,64 @@ 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
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -71,7 +71,7 @@ class ChannelIntro extends PureComponent {
|
||||
style={style.profile}
|
||||
>
|
||||
<ProfilePicture
|
||||
userId={member.id}
|
||||
user={member}
|
||||
size={64}
|
||||
statusBorderWidth={2}
|
||||
statusSize={25}
|
||||
|
||||
105
app/components/channel_loader.js
Normal file
105
app/components/channel_loader.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// 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 {
|
||||
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 {
|
||||
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);
|
||||
@@ -40,7 +40,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}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,8 +15,8 @@ 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
|
||||
@@ -24,138 +24,49 @@ export default class Emoji extends React.PureComponent {
|
||||
|
||||
static defaultProps = {
|
||||
customEmojis: new Map(),
|
||||
literal: ''
|
||||
literal: '',
|
||||
padding: 10
|
||||
};
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
fontSize,
|
||||
customEmojis,
|
||||
emojiName,
|
||||
literal,
|
||||
padding,
|
||||
size,
|
||||
textStyle,
|
||||
token
|
||||
} = this.props;
|
||||
|
||||
if (!this.state.imageUrl) {
|
||||
let imageUrl;
|
||||
if (EmojiIndicesByAlias.has(emojiName)) {
|
||||
const emoji = Emojis[EmojiIndicesByAlias.get(emojiName)];
|
||||
imageUrl = Client4.getSystemEmojiImageUrl(emoji.filename);
|
||||
} else if (customEmojis.has(emojiName)) {
|
||||
const emoji = customEmojis.get(emojiName);
|
||||
imageUrl = Client4.getCustomEmojiImageUrl(emoji.id);
|
||||
}
|
||||
|
||||
if (!imageUrl) {
|
||||
return <Text style={textStyle}>{literal}</Text>;
|
||||
}
|
||||
|
||||
let ImageComponent;
|
||||
if (Platform.OS === 'android') {
|
||||
ImageComponent = Image;
|
||||
} else {
|
||||
ImageComponent = FastImage;
|
||||
}
|
||||
|
||||
let ImageComponent = FastImage;
|
||||
const source = {
|
||||
uri: this.state.imageUrl,
|
||||
uri: 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;
|
||||
}
|
||||
if (Platform.OS === 'android') {
|
||||
ImageComponent = Image;
|
||||
}
|
||||
|
||||
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}}
|
||||
style={{width: size, height: size, padding}}
|
||||
source={source}
|
||||
onError={this.onError}
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {
|
||||
Dimensions,
|
||||
SectionList,
|
||||
TouchableOpacity,
|
||||
View
|
||||
@@ -16,6 +17,7 @@ import SearchBar from 'app/components/search_bar';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
|
||||
|
||||
const {width: deviceWidth} = Dimensions.get('window');
|
||||
const EMOJI_SIZE = 30;
|
||||
const EMOJI_GUTTER = 7.5;
|
||||
const SECTION_MARGIN = 15;
|
||||
@@ -23,7 +25,6 @@ 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
|
||||
@@ -42,19 +43,10 @@ class EmojiPicker extends PureComponent {
|
||||
|
||||
this.state = {
|
||||
emojis: props.emojis,
|
||||
searchTerm: '',
|
||||
width: props.deviceWidth - (SECTION_MARGIN * 2)
|
||||
searchTerm: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.deviceWidth !== this.props.deviceWidth) {
|
||||
this.setState({
|
||||
width: nextProps.deviceWidth - (SECTION_MARGIN * 2)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
changeSearchTerm = (text) => {
|
||||
this.setState({
|
||||
searchTerm: text
|
||||
@@ -75,11 +67,11 @@ class EmojiPicker extends PureComponent {
|
||||
emojis: this.props.emojis,
|
||||
searchTerm: ''
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
filterEmojiAliases = (aliases, searchTerm) => {
|
||||
return aliases.findIndex((alias) => alias.includes(searchTerm)) !== -1;
|
||||
};
|
||||
}
|
||||
|
||||
searchEmojis = (searchTerm) => {
|
||||
const {emojis} = this.props;
|
||||
@@ -114,7 +106,7 @@ class EmojiPicker extends PureComponent {
|
||||
});
|
||||
|
||||
return nextEmojis;
|
||||
};
|
||||
}
|
||||
|
||||
renderSectionHeader = ({section}) => {
|
||||
const {theme} = this.props;
|
||||
@@ -129,7 +121,7 @@ class EmojiPicker extends PureComponent {
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
renderEmojis = (emojis, index) => {
|
||||
const {theme} = this.props;
|
||||
@@ -165,13 +157,13 @@ class EmojiPicker extends PureComponent {
|
||||
})}
|
||||
</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 numColumns = Number(((deviceWidth - (SECTION_MARGIN * 2)) / (EMOJI_SIZE + (EMOJI_GUTTER * 2))).toFixed(0));
|
||||
|
||||
const slices = item.items.reduce((slice, emoji, emojiIndex) => {
|
||||
if (emojiIndex % numColumns === 0 && emojiIndex !== 0) {
|
||||
@@ -188,7 +180,7 @@ class EmojiPicker extends PureComponent {
|
||||
{slices.map(this.renderEmojis)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {intl, theme} = this.props;
|
||||
@@ -261,7 +253,8 @@ const getStyleSheetFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
marginRight: 0
|
||||
},
|
||||
listView: {
|
||||
backgroundColor: theme.centerChannelBg
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
width: deviceWidth - (SECTION_MARGIN * 2)
|
||||
},
|
||||
searchBar: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
|
||||
@@ -6,7 +6,6 @@ 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';
|
||||
|
||||
@@ -99,11 +98,9 @@ const getEmojisBySection = createSelector(
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const emojis = getEmojisBySection(state);
|
||||
const {deviceWidth} = getDimensions(state);
|
||||
|
||||
return {
|
||||
emojis,
|
||||
deviceWidth,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -64,7 +64,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}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -167,14 +157,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 +169,6 @@ export default class Markdown extends PureComponent {
|
||||
emojiName={emojiName}
|
||||
literal={literal}
|
||||
size={size}
|
||||
fontSize={fontSize}
|
||||
textStyle={this.computeTextStyle(this.props.baseTextStyle, context)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -49,7 +49,6 @@ class Post extends PureComponent {
|
||||
navigator: PropTypes.object,
|
||||
roles: PropTypes.string,
|
||||
shouldRenderReplyButton: PropTypes.bool,
|
||||
showFullDate: PropTypes.bool,
|
||||
tooltipVisible: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onPress: PropTypes.func,
|
||||
@@ -157,24 +156,24 @@ class Post extends PureComponent {
|
||||
const {intl, navigator, post, theme} = this.props;
|
||||
|
||||
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).
|
||||
then((source) => {
|
||||
navigator.showModal({
|
||||
screen: 'AddReaction',
|
||||
title: intl.formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
|
||||
animated: true,
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
navBarBackgroundColor: theme.sidebarHeaderBg,
|
||||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg
|
||||
},
|
||||
passProps: {
|
||||
post,
|
||||
closeButton: source,
|
||||
onEmojiPress: this.handleAddReactionToPost
|
||||
}
|
||||
});
|
||||
then((source) => {
|
||||
navigator.showModal({
|
||||
screen: 'AddReaction',
|
||||
title: intl.formatMessage({id: 'mobile.post_info.add_reaction', defaultMessage: 'Add Reaction'}),
|
||||
animated: true,
|
||||
navigatorStyle: {
|
||||
navBarTextColor: theme.sidebarHeaderTextColor,
|
||||
navBarBackgroundColor: theme.sidebarHeaderBg,
|
||||
navBarButtonColor: theme.sidebarHeaderTextColor,
|
||||
screenBackgroundColor: theme.centerChannelBg
|
||||
},
|
||||
passProps: {
|
||||
post,
|
||||
closeButton: source,
|
||||
onEmojiPress: this.handleAddReactionToPost
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleFailedPostPress = () => {
|
||||
@@ -281,17 +280,15 @@ class Post extends PureComponent {
|
||||
};
|
||||
|
||||
viewUserProfile = () => {
|
||||
const {isSearchResult, tooltipVisible} = this.props;
|
||||
const {isSearchResult} = this.props;
|
||||
|
||||
if (!isSearchResult && !tooltipVisible) {
|
||||
if (!isSearchResult) {
|
||||
preventDoubleTap(this.goToUserProfile, this);
|
||||
}
|
||||
};
|
||||
|
||||
toggleSelected = (selected, tooltip) => {
|
||||
if (tooltip) {
|
||||
this.props.actions.setPostTooltipVisible(selected);
|
||||
}
|
||||
toggleSelected = (selected) => {
|
||||
this.props.actions.setPostTooltipVisible(selected);
|
||||
this.setState({selected});
|
||||
};
|
||||
|
||||
@@ -304,7 +301,6 @@ class Post extends PureComponent {
|
||||
post,
|
||||
renderReplies,
|
||||
shouldRenderReplyButton,
|
||||
showFullDate,
|
||||
theme
|
||||
} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
@@ -328,7 +324,6 @@ class Post extends PureComponent {
|
||||
createAt={post.create_at}
|
||||
isSearchResult={isSearchResult}
|
||||
shouldRenderReplyButton={shouldRenderReplyButton}
|
||||
showFullDate={showFullDate}
|
||||
onPress={this.handleReply}
|
||||
onViewUserProfile={this.viewUserProfile}
|
||||
renderReplies={renderReplies}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {getOpenGraphMetadata} from 'mattermost-redux/actions/posts';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import PostAttachmentOpenGraph from './post_attachment_opengraph';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
return {
|
||||
...ownProps,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getOpenGraphMetadata
|
||||
}, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PostAttachmentOpenGraph);
|
||||
@@ -1,252 +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 {
|
||||
Dimensions,
|
||||
Image,
|
||||
Linking,
|
||||
PixelRatio,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import {getNearestPoint} from 'app/utils/opengraph';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const LARGE_IMAGE_MIN_WIDTH = 150;
|
||||
const LARGE_IMAGE_MIN_RATIO = (16 / 9);
|
||||
const MAX_IMAGE_HEIGHT = 150;
|
||||
const THUMBNAIL_SIZE = 75;
|
||||
|
||||
export default class PostAttachmentOpenGraph extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
getOpenGraphMetadata: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
link: PropTypes.string.isRequired,
|
||||
openGraphData: PropTypes.object,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
imageLoaded: false,
|
||||
hasLargeImage: false
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.fetchData(this.props.link, this.props.openGraphData);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.link !== this.props.link) {
|
||||
this.setState({imageLoaded: false});
|
||||
this.fetchData(nextProps.link, nextProps.openGraphData);
|
||||
}
|
||||
}
|
||||
|
||||
calculateLargeImageDimensions = (width, height) => {
|
||||
const {width: deviceWidth} = Dimensions.get('window');
|
||||
let maxHeight = MAX_IMAGE_HEIGHT;
|
||||
let maxWidth = deviceWidth - 88;
|
||||
|
||||
if (height <= MAX_IMAGE_HEIGHT) {
|
||||
maxHeight = height;
|
||||
} else {
|
||||
maxHeight = (height / width) * maxWidth;
|
||||
if (maxHeight > MAX_IMAGE_HEIGHT) {
|
||||
maxHeight = MAX_IMAGE_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
if (height > width) {
|
||||
maxWidth = (width / height) * maxHeight;
|
||||
}
|
||||
|
||||
return {width: maxWidth, height: maxHeight};
|
||||
};
|
||||
|
||||
calculateSmallImageDimensions = (width, height) => {
|
||||
const {width: deviceWidth} = Dimensions.get('window');
|
||||
const offset = deviceWidth - 170;
|
||||
|
||||
let ratio;
|
||||
let maxWidth;
|
||||
let maxHeight;
|
||||
|
||||
if (width >= height) {
|
||||
ratio = width / height;
|
||||
maxWidth = THUMBNAIL_SIZE;
|
||||
maxHeight = PixelRatio.roundToNearestPixel(maxWidth / ratio);
|
||||
} else {
|
||||
ratio = height / width;
|
||||
maxHeight = THUMBNAIL_SIZE;
|
||||
maxWidth = PixelRatio.roundToNearestPixel(maxHeight / ratio);
|
||||
}
|
||||
|
||||
return {width: maxWidth, height: maxHeight, offset};
|
||||
};
|
||||
|
||||
fetchData(url, openGraphData) {
|
||||
if (!openGraphData) {
|
||||
this.props.actions.getOpenGraphMetadata(url);
|
||||
}
|
||||
}
|
||||
|
||||
getBestImageUrl(data) {
|
||||
if (!data.images) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bestDimensions = {
|
||||
width: Dimensions.get('window').width - 88,
|
||||
height: MAX_IMAGE_HEIGHT
|
||||
};
|
||||
const bestImage = getNearestPoint(bestDimensions, data.images, 'width', 'height');
|
||||
return bestImage.secure_url || bestImage.url;
|
||||
}
|
||||
|
||||
getImageSize = (imageUrl) => {
|
||||
if (!this.state.imageLoaded) {
|
||||
Image.getSize(imageUrl, (width, height) => {
|
||||
const {hasLargeImage} = this.state;
|
||||
const imageRatio = width / height;
|
||||
|
||||
let isLarge = false;
|
||||
let dimensions;
|
||||
if (width >= LARGE_IMAGE_MIN_WIDTH && imageRatio >= LARGE_IMAGE_MIN_RATIO && !hasLargeImage) {
|
||||
isLarge = true;
|
||||
dimensions = this.calculateLargeImageDimensions(width, height);
|
||||
} else {
|
||||
dimensions = this.calculateSmallImageDimensions(width, height);
|
||||
}
|
||||
this.setState({
|
||||
...dimensions,
|
||||
hasLargeImage: isLarge,
|
||||
imageLoaded: true
|
||||
});
|
||||
}, () => null);
|
||||
}
|
||||
};
|
||||
|
||||
goToLink = () => {
|
||||
Linking.openURL(this.props.link);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {openGraphData, theme} = this.props;
|
||||
const {hasLargeImage, height, imageLoaded, offset, width} = this.state;
|
||||
|
||||
if (!openGraphData || !openGraphData.description) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const imageUrl = this.getBestImageUrl(openGraphData);
|
||||
const isThumbnail = !hasLargeImage && imageLoaded;
|
||||
if (imageUrl) {
|
||||
this.getImageSize(imageUrl);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<View style={style.flex}>
|
||||
<Text
|
||||
style={style.siteTitle}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
>
|
||||
{openGraphData.site_name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={style.wrapper}>
|
||||
<TouchableOpacity
|
||||
style={isThumbnail ? {width: offset} : style.flex}
|
||||
onPress={this.goToLink}
|
||||
>
|
||||
<Text
|
||||
style={style.siteSubtitle}
|
||||
numberOfLines={3}
|
||||
ellipsizeMode='tail'
|
||||
>
|
||||
{openGraphData.title}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{isThumbnail &&
|
||||
<View style={style.thumbnail}>
|
||||
<Image
|
||||
style={[style.image, {width, height}]}
|
||||
source={{uri: imageUrl}}
|
||||
resizeMode='cover'
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
<View style={style.flex}>
|
||||
<Text
|
||||
style={style.siteDescription}
|
||||
numberOfLines={5}
|
||||
ellipsizeMode='tail'
|
||||
>
|
||||
{openGraphData.description}
|
||||
</Text>
|
||||
</View>
|
||||
{hasLargeImage && imageLoaded &&
|
||||
<Image
|
||||
style={[style.image, {width, height}]}
|
||||
source={{uri: imageUrl}}
|
||||
resizeMode='cover'
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
borderWidth: 1,
|
||||
marginTop: 10,
|
||||
padding: 10
|
||||
},
|
||||
flex: {
|
||||
flex: 1
|
||||
},
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
siteTitle: {
|
||||
fontSize: 12,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
marginBottom: 10
|
||||
},
|
||||
siteSubtitle: {
|
||||
fontSize: 14,
|
||||
color: theme.linkColor,
|
||||
marginBottom: 10
|
||||
},
|
||||
siteDescription: {
|
||||
fontSize: 13,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.7),
|
||||
marginBottom: 10
|
||||
},
|
||||
image: {
|
||||
borderRadius: 3
|
||||
},
|
||||
thumbnail: {
|
||||
flex: 1,
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'flex-start'
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import {flagPost, unflagPost} from 'mattermost-redux/actions/posts';
|
||||
import {Posts} from 'mattermost-redux/constants';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {isPostFlagged, isPostEphemeral, isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
import {isPostFlagged, isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
@@ -20,14 +20,13 @@ function mapStateToProps(state, ownProps) {
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
postProps: post.props || {},
|
||||
attachments: post.props && post.props.attachments,
|
||||
fileIds: post.file_ids,
|
||||
hasBeenDeleted: post.state === Posts.POST_DELETED,
|
||||
hasReactions: post.has_reactions,
|
||||
isFailed: post.failed,
|
||||
isFlagged: isPostFlagged(post.id, myPreferences),
|
||||
isPending: post.id === post.pending_post_id,
|
||||
isPostEphemeral: isPostEphemeral(post),
|
||||
isSystemMessage: isSystemMessage(post),
|
||||
message: post.message,
|
||||
theme: getTheme(state)
|
||||
|
||||
@@ -16,8 +16,7 @@ import FileAttachmentList from 'app/components/file_attachment_list';
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import Markdown from 'app/components/markdown';
|
||||
import OptionsContext from 'app/components/options_context';
|
||||
|
||||
import PostBodyAdditionalContent from 'app/components/post_body_additional_content';
|
||||
import SlackAttachments from 'app/components/slack_attachments';
|
||||
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import {getMarkdownTextStyles, getMarkdownBlockStyles} from 'app/utils/markdown';
|
||||
@@ -30,6 +29,7 @@ class PostBody extends PureComponent {
|
||||
flagPost: PropTypes.func.isRequired,
|
||||
unflagPost: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
attachments: PropTypes.array,
|
||||
canDelete: PropTypes.bool,
|
||||
canEdit: PropTypes.bool,
|
||||
fileIds: PropTypes.array,
|
||||
@@ -39,7 +39,6 @@ class PostBody extends PureComponent {
|
||||
isFailed: PropTypes.bool,
|
||||
isFlagged: PropTypes.bool,
|
||||
isPending: PropTypes.bool,
|
||||
isPostEphemeral: PropTypes.bool,
|
||||
isSearchResult: PropTypes.bool,
|
||||
isSystemMessage: PropTypes.bool,
|
||||
message: PropTypes.string,
|
||||
@@ -50,7 +49,6 @@ class PostBody extends PureComponent {
|
||||
onPostEdit: PropTypes.func,
|
||||
onPress: PropTypes.func,
|
||||
postId: PropTypes.string.isRequired,
|
||||
postProps: PropTypes.object,
|
||||
renderReplyBar: PropTypes.func,
|
||||
theme: PropTypes.object,
|
||||
toggleSelected: PropTypes.func
|
||||
@@ -125,6 +123,25 @@ class PostBody extends PureComponent {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
renderSlackAttachments = (baseStyle, blockStyles, textStyles) => {
|
||||
const {attachments, navigator, theme} = this.props;
|
||||
|
||||
if (attachments && attachments.length) {
|
||||
return (
|
||||
<SlackAttachments
|
||||
attachments={attachments}
|
||||
baseTextStyle={baseStyle}
|
||||
blockStyles={blockStyles}
|
||||
navigator={navigator}
|
||||
textStyles={textStyles}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
canDelete,
|
||||
@@ -134,7 +151,6 @@ class PostBody extends PureComponent {
|
||||
isFailed,
|
||||
isFlagged,
|
||||
isPending,
|
||||
isPostEphemeral,
|
||||
isSearchResult,
|
||||
isSystemMessage,
|
||||
intl,
|
||||
@@ -145,7 +161,6 @@ class PostBody extends PureComponent {
|
||||
onPostEdit,
|
||||
onPress,
|
||||
postId,
|
||||
postProps,
|
||||
renderReplyBar,
|
||||
theme,
|
||||
toggleSelected
|
||||
@@ -159,7 +174,7 @@ class PostBody extends PureComponent {
|
||||
const isPendingOrFailedPost = isPending || isFailed;
|
||||
|
||||
// we should check for the user roles and permissions
|
||||
if (!isPendingOrFailedPost && !isSearchResult && !isSystemMessage && !isPostEphemeral) {
|
||||
if (!isPendingOrFailedPost && !isSearchResult) {
|
||||
if (isFlagged) {
|
||||
actions.push({
|
||||
text: formatMessage({id: 'post_info.mobile.unflag', defaultMessage: 'Unflag'}),
|
||||
@@ -186,7 +201,6 @@ class PostBody extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
let body;
|
||||
let messageComponent;
|
||||
if (hasBeenDeleted) {
|
||||
messageComponent = (
|
||||
@@ -196,7 +210,6 @@ class PostBody extends PureComponent {
|
||||
defaultMessage='(message deleted)'
|
||||
/>
|
||||
);
|
||||
body = (<View>{messageComponent}</View>);
|
||||
} else if (message.length) {
|
||||
messageComponent = (
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
@@ -216,52 +229,38 @@ class PostBody extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasBeenDeleted) {
|
||||
if (isSearchResult) {
|
||||
body = (
|
||||
<TouchableHighlight
|
||||
onHideUnderlay={this.handleHideUnderlay}
|
||||
onPress={onPress}
|
||||
onShowUnderlay={this.handleShowUnderlay}
|
||||
underlayColor='transparent'
|
||||
>
|
||||
<View>
|
||||
{messageComponent}
|
||||
<PostBodyAdditionalContent
|
||||
baseTextStyle={messageStyle}
|
||||
blockStyles={blockStyles}
|
||||
navigator={navigator}
|
||||
message={message}
|
||||
postProps={postProps}
|
||||
textStyles={textStyles}
|
||||
/>
|
||||
{this.renderFileAttachments()}
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<OptionsContext
|
||||
actions={actions}
|
||||
ref='options'
|
||||
onPress={onPress}
|
||||
toggleSelected={toggleSelected}
|
||||
cancelText={formatMessage({id: 'channel_modal.cancel', defaultMessage: 'Cancel'})}
|
||||
>
|
||||
let body;
|
||||
if (isSearchResult) {
|
||||
body = (
|
||||
<TouchableHighlight
|
||||
onHideUnderlay={this.handleHideUnderlay}
|
||||
onLongPress={this.show}
|
||||
onPress={onPress}
|
||||
onShowUnderlay={this.handleShowUnderlay}
|
||||
underlayColor='transparent'
|
||||
>
|
||||
<View>
|
||||
{messageComponent}
|
||||
<PostBodyAdditionalContent
|
||||
baseTextStyle={messageStyle}
|
||||
blockStyles={blockStyles}
|
||||
navigator={navigator}
|
||||
message={message}
|
||||
postProps={postProps}
|
||||
textStyles={textStyles}
|
||||
/>
|
||||
{this.renderSlackAttachments(messageStyle, blockStyles, textStyles)}
|
||||
{this.renderFileAttachments()}
|
||||
{hasReactions && <Reactions postId={postId}/>}
|
||||
</OptionsContext>
|
||||
);
|
||||
}
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<OptionsContext
|
||||
actions={actions}
|
||||
ref='options'
|
||||
onPress={onPress}
|
||||
toggleSelected={toggleSelected}
|
||||
cancelText={formatMessage({id: 'channel_modal.cancel', defaultMessage: 'Cancel'})}
|
||||
>
|
||||
{messageComponent}
|
||||
{this.renderSlackAttachments(messageStyle, blockStyles, textStyles)}
|
||||
{this.renderFileAttachments()}
|
||||
{hasReactions && <Reactions postId={postId}/>}
|
||||
</OptionsContext>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -293,8 +292,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
message: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 15,
|
||||
lineHeight: 20
|
||||
fontSize: 15
|
||||
},
|
||||
messageContainerWithReplyBar: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getOpenGraphMetadataForUrl} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {getDimensions} from 'app/selectors/device';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
import {extractFirstLink} from 'app/utils/url';
|
||||
|
||||
import PostBodyAdditionalContent from './post_body_additional_content';
|
||||
|
||||
function makeGetFirstLink() {
|
||||
let link;
|
||||
let lastMessage;
|
||||
|
||||
return (message) => {
|
||||
if (message !== lastMessage) {
|
||||
link = extractFirstLink(message);
|
||||
lastMessage = message;
|
||||
}
|
||||
|
||||
return link;
|
||||
};
|
||||
}
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getFirstLink = makeGetFirstLink();
|
||||
|
||||
return function mapStateToProps(state, ownProps) {
|
||||
const config = getConfig(state);
|
||||
const previewsEnabled = getBool(state, Preferences.CATEGORY_ADVANCED_SETTINGS, `${ViewTypes.FEATURE_TOGGLE_PREFIX}${ViewTypes.EMBED_PREVIEW}`);
|
||||
const link = getFirstLink(ownProps.message);
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
...getDimensions(state),
|
||||
config,
|
||||
link,
|
||||
openGraphData: getOpenGraphMetadataForUrl(state, link),
|
||||
showLinkPreviews: previewsEnabled && config.EnableLinkPreviews === 'true',
|
||||
theme: getTheme(state)
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(PostBodyAdditionalContent);
|
||||
@@ -1,284 +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 {
|
||||
Image,
|
||||
Linking,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableWithoutFeedback,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import {YouTubeStandaloneAndroid, YouTubeStandaloneIOS} from 'react-native-youtube';
|
||||
import youTubeVideoId from 'youtube-video-id';
|
||||
import youtubePlayIcon from 'assets/images/icons/youtube-play-icon.png';
|
||||
|
||||
import PostAttachmentOpenGraph from 'app/components/post_attachment_opengraph';
|
||||
import SlackAttachments from 'app/components/slack_attachments';
|
||||
import CustomPropTypes from 'app/constants/custom_prop_types';
|
||||
import {isImageLink, isYoutubeLink} from 'app/utils/url';
|
||||
|
||||
const MAX_IMAGE_HEIGHT = 150;
|
||||
|
||||
export default class PostBodyAdditionalContent extends PureComponent {
|
||||
static propTypes = {
|
||||
baseTextStyle: CustomPropTypes.Style,
|
||||
blockStyles: PropTypes.object,
|
||||
config: PropTypes.object,
|
||||
deviceHeight: PropTypes.number.isRequired,
|
||||
deviceWidth: PropTypes.number.isRequired,
|
||||
link: PropTypes.string,
|
||||
message: PropTypes.string.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
openGraphData: PropTypes.object,
|
||||
postProps: PropTypes.object.isRequired,
|
||||
showLinkPreviews: PropTypes.bool.isRequired,
|
||||
textStyles: PropTypes.object,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
linkLoadError: false,
|
||||
linkLoaded: false
|
||||
};
|
||||
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.getImageSize();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.message !== this.props.message) {
|
||||
this.setState({
|
||||
linkLoadError: false,
|
||||
linkLoaded: false
|
||||
}, () => {
|
||||
this.getImageSize();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
calculateDimensions = (width, height) => {
|
||||
const {deviceHeight, deviceWidth} = this.props;
|
||||
let maxHeight = MAX_IMAGE_HEIGHT;
|
||||
const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth;
|
||||
let maxWidth = deviceSize - 78;
|
||||
|
||||
if (height <= MAX_IMAGE_HEIGHT) {
|
||||
maxHeight = height;
|
||||
} else {
|
||||
maxHeight = (height / width) * maxWidth;
|
||||
if (maxHeight > MAX_IMAGE_HEIGHT) {
|
||||
maxHeight = MAX_IMAGE_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
if (height > width) {
|
||||
maxWidth = (width / height) * maxHeight;
|
||||
}
|
||||
|
||||
return {width: maxWidth, height: maxHeight};
|
||||
};
|
||||
|
||||
generateStaticEmbed = (isYouTube, isImage) => {
|
||||
if (isYouTube || isImage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {link, openGraphData, showLinkPreviews} = this.props;
|
||||
const attachments = this.getSlackAttachment();
|
||||
if (attachments) {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
if (link && showLinkPreviews) {
|
||||
return (
|
||||
<PostAttachmentOpenGraph
|
||||
link={link}
|
||||
openGraphData={openGraphData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
getImageSize = () => {
|
||||
const {link} = this.props;
|
||||
const {linkLoaded} = this.state;
|
||||
|
||||
if (link) {
|
||||
let imageUrl;
|
||||
if (isImageLink(link)) {
|
||||
imageUrl = link;
|
||||
} else if (isYoutubeLink(link)) {
|
||||
const videoId = youTubeVideoId(link);
|
||||
imageUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
|
||||
}
|
||||
|
||||
if (imageUrl && !linkLoaded) {
|
||||
Image.getSize(imageUrl, (width, height) => {
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dimensions = this.calculateDimensions(width, height);
|
||||
this.setState({...dimensions, linkLoaded: true});
|
||||
}, () => null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getSlackAttachment = () => {
|
||||
const {
|
||||
postProps,
|
||||
baseTextStyle,
|
||||
blockStyles,
|
||||
navigator,
|
||||
textStyles,
|
||||
theme
|
||||
} = this.props;
|
||||
const {attachments} = postProps;
|
||||
|
||||
if (attachments && attachments.length) {
|
||||
return (
|
||||
<SlackAttachments
|
||||
attachments={attachments}
|
||||
baseTextStyle={baseTextStyle}
|
||||
blockStyles={blockStyles}
|
||||
navigator={navigator}
|
||||
textStyles={textStyles}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
generateToggleableEmbed = (isImage, isYouTube) => {
|
||||
const {link} = this.props;
|
||||
const {width, height} = this.state;
|
||||
|
||||
if (link) {
|
||||
if (isYouTube) {
|
||||
const videoId = youTubeVideoId(link);
|
||||
const imgUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
style={styles.imageContainer}
|
||||
{...this.responder}
|
||||
onPress={this.playYouTubeVideo}
|
||||
>
|
||||
<Image
|
||||
style={[styles.image, {width, height}]}
|
||||
source={{uri: imgUrl}}
|
||||
resizeMode={'cover'}
|
||||
onError={this.handleLinkLoadError}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={this.playYouTubeVideo}>
|
||||
<Image
|
||||
source={youtubePlayIcon}
|
||||
onPress={this.playYouTubeVideo}
|
||||
/>
|
||||
</TouchableWithoutFeedback>
|
||||
</Image>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
if (isImage) {
|
||||
return (
|
||||
<View style={styles.imageContainer}>
|
||||
<Image
|
||||
style={[styles.image, {width, height}]}
|
||||
source={{uri: link}}
|
||||
resizeMode={'cover'}
|
||||
onError={this.handleLinkLoadError}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
playYouTubeVideo = () => {
|
||||
const {link} = this.props;
|
||||
const videoId = youTubeVideoId(link);
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
YouTubeStandaloneIOS.playVideo(videoId);
|
||||
} else {
|
||||
const {config} = this.props;
|
||||
|
||||
if (config.GoogleDeveloperKey) {
|
||||
YouTubeStandaloneAndroid.playVideo({
|
||||
apiKey: config.GoogleDeveloperKey,
|
||||
videoId,
|
||||
autoplay: true
|
||||
});
|
||||
} else {
|
||||
Linking.openURL(link);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleLinkLoadError = () => {
|
||||
this.setState({linkLoadError: true});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {link, openGraphData} = this.props;
|
||||
const {linkLoaded, linkLoadError} = this.state;
|
||||
let isYouTube = false;
|
||||
let isImage = false;
|
||||
let isOpenGraph = false;
|
||||
|
||||
if (link) {
|
||||
isYouTube = isYoutubeLink(link);
|
||||
isImage = isImageLink(link);
|
||||
isOpenGraph = Boolean(openGraphData && openGraphData.description);
|
||||
|
||||
if (((isImage && !isOpenGraph) || isYouTube) && !linkLoadError) {
|
||||
const embed = this.generateToggleableEmbed(isImage, isYouTube);
|
||||
if (embed && (linkLoaded || isYouTube)) {
|
||||
return embed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.generateStaticEmbed(isYouTube, isImage);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
imageContainer: {
|
||||
alignItems: 'flex-start',
|
||||
flex: 1,
|
||||
justifyContent: 'flex-start',
|
||||
marginBottom: 6,
|
||||
marginTop: 10
|
||||
},
|
||||
image: {
|
||||
alignItems: 'center',
|
||||
borderRadius: 3,
|
||||
justifyContent: 'center',
|
||||
marginVertical: 1
|
||||
}
|
||||
});
|
||||
@@ -3,9 +3,8 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getPost, makeGetCommentCountForPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getBool, getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
@@ -22,7 +21,6 @@ function makeMapStateToProps() {
|
||||
const commentedOnUser = getUser(state, ownProps.commentedOnUserId);
|
||||
const user = getUser(state, post.user_id);
|
||||
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
|
||||
const militaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time');
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
@@ -32,7 +30,6 @@ function makeMapStateToProps() {
|
||||
displayName: displayUsername(user, teammateNameDisplay),
|
||||
enablePostUsernameOverride: config.EnablePostUsernameOverride === 'true',
|
||||
fromWebHook: post.props && post.props.from_webhook === 'true',
|
||||
militaryTime,
|
||||
isPendingOrFailedPost: isPostPendingOrFailed(post),
|
||||
isSystemMessage: isSystemMessage(post),
|
||||
overrideUsername: post.props && post.props.override_username,
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import FormattedTime from 'app/components/formatted_time';
|
||||
import FormattedDate from 'app/components/formatted_date';
|
||||
import ReplyIcon from 'app/components/reply_icon';
|
||||
import {emptyFunction} from 'app/utils/general';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
@@ -28,14 +27,12 @@ export default class PostHeader extends PureComponent {
|
||||
fromWebHook: PropTypes.bool,
|
||||
isPendingOrFailedPost: PropTypes.bool,
|
||||
isSearchResult: PropTypes.bool,
|
||||
shouldRenderReplyButton: PropTypes.bool,
|
||||
isSystemMessage: PropTypes.bool,
|
||||
militaryTime: PropTypes.bool,
|
||||
onPress: PropTypes.func,
|
||||
onViewUserProfile: PropTypes.func,
|
||||
overrideUsername: PropTypes.string,
|
||||
renderReplies: PropTypes.bool,
|
||||
shouldRenderReplyButton: PropTypes.bool,
|
||||
showFullDate: PropTypes.bool,
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
@@ -143,49 +140,23 @@ export default class PostHeader extends PureComponent {
|
||||
createAt,
|
||||
isPendingOrFailedPost,
|
||||
isSearchResult,
|
||||
militaryTime,
|
||||
onPress,
|
||||
renderReplies,
|
||||
shouldRenderReplyButton,
|
||||
showFullDate,
|
||||
theme
|
||||
} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const showReply = shouldRenderReplyButton || (!commentedOnDisplayName && commentCount > 0 && renderReplies);
|
||||
|
||||
let dateComponent;
|
||||
if (showFullDate) {
|
||||
dateComponent = (
|
||||
<View style={style.datetime}>
|
||||
<Text style={style.time}>
|
||||
<FormattedDate value={createAt}/>
|
||||
</Text>
|
||||
<Text style={style.time}>
|
||||
<FormattedTime
|
||||
hour12={!militaryTime}
|
||||
value={createAt}
|
||||
/>
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
dateComponent = (
|
||||
<Text style={style.time}>
|
||||
<FormattedTime
|
||||
hour12={!militaryTime}
|
||||
value={createAt}
|
||||
/>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={[style.postInfoContainer, (isPendingOrFailedPost && style.pendingPost)]}>
|
||||
<View style={{flexDirection: 'row', flex: 1}}>
|
||||
{this.getDisplayName(style)}
|
||||
<View style={style.timeContainer}>
|
||||
{dateComponent}
|
||||
<Text style={style.time}>
|
||||
<FormattedTime value={createAt}/>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{showReply &&
|
||||
@@ -232,10 +203,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
timeContainer: {
|
||||
justifyContent: 'center'
|
||||
},
|
||||
datetime: {
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
time: {
|
||||
color: theme.centerChannelColor,
|
||||
fontSize: 13,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
StyleSheet,
|
||||
View
|
||||
} from 'react-native';
|
||||
import FlatList from 'app/components/inverted_flat_list';
|
||||
@@ -25,7 +24,8 @@ export default class PostList extends PureComponent {
|
||||
actions: PropTypes.shape({
|
||||
refreshChannelWithRetry: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
channelId: PropTypes.string,
|
||||
channel: PropTypes.object,
|
||||
channelIsLoading: PropTypes.bool.isRequired,
|
||||
currentUserId: PropTypes.string,
|
||||
indicateNewMessages: PropTypes.bool,
|
||||
isLoadingMore: PropTypes.bool,
|
||||
@@ -34,7 +34,6 @@ export default class PostList extends PureComponent {
|
||||
loadMore: PropTypes.func,
|
||||
navigator: PropTypes.object,
|
||||
onPostPress: PropTypes.func,
|
||||
onRefresh: PropTypes.func,
|
||||
posts: PropTypes.array.isRequired,
|
||||
refreshing: PropTypes.bool,
|
||||
renderReplies: PropTypes.bool,
|
||||
@@ -43,6 +42,11 @@ export default class PostList extends PureComponent {
|
||||
theme: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
channel: {},
|
||||
channelIsLoading: false
|
||||
};
|
||||
|
||||
getPostsWithDates = () => {
|
||||
const {posts, indicateNewMessages, currentUserId, lastViewedAt, showLoadMore} = this.props;
|
||||
const list = addDatesToPostList(posts, {indicateNewMessages, currentUserId, lastViewedAt});
|
||||
@@ -73,25 +77,17 @@ export default class PostList extends PureComponent {
|
||||
};
|
||||
|
||||
onRefresh = () => {
|
||||
const {
|
||||
actions,
|
||||
channelId,
|
||||
onRefresh
|
||||
} = this.props;
|
||||
const {actions, channel} = this.props;
|
||||
|
||||
if (channelId) {
|
||||
actions.refreshChannelWithRetry(channelId);
|
||||
}
|
||||
|
||||
if (onRefresh) {
|
||||
onRefresh();
|
||||
if (Object.keys(channel).length) {
|
||||
actions.refreshChannelWithRetry(channel.id);
|
||||
}
|
||||
};
|
||||
|
||||
renderChannelIntro = () => {
|
||||
const {channelId, navigator, refreshing, showLoadMore} = this.props;
|
||||
const {channel, channelIsLoading, navigator, refreshing, showLoadMore} = this.props;
|
||||
|
||||
if (channelId && !showLoadMore && !refreshing) {
|
||||
if (channel.hasOwnProperty('id') && !showLoadMore && !refreshing && !channelIsLoading) {
|
||||
return (
|
||||
<View>
|
||||
<ChannelIntro navigator={navigator}/>
|
||||
@@ -134,10 +130,6 @@ export default class PostList extends PureComponent {
|
||||
return this.renderPost(item);
|
||||
};
|
||||
|
||||
getItem = (data, index) => data[index];
|
||||
|
||||
getItemCount = (data) => data.length;
|
||||
|
||||
renderPost = (post) => {
|
||||
const {
|
||||
isSearchResult,
|
||||
@@ -163,13 +155,13 @@ export default class PostList extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {channelId, refreshing, theme} = this.props;
|
||||
const {channel, refreshing, theme} = this.props;
|
||||
|
||||
const refreshControl = {
|
||||
refreshing
|
||||
};
|
||||
|
||||
if (channelId) {
|
||||
if (Object.keys(channel).length) {
|
||||
refreshControl.onRefresh = this.onRefresh;
|
||||
}
|
||||
|
||||
@@ -181,20 +173,11 @@ export default class PostList extends PureComponent {
|
||||
keyExtractor={this.keyExtractor}
|
||||
ListFooterComponent={this.renderChannelIntro}
|
||||
onEndReached={this.loadMorePosts}
|
||||
onEndReachedThreshold={0}
|
||||
onEndReachedThreshold={700}
|
||||
{...refreshControl}
|
||||
renderItem={this.renderItem}
|
||||
theme={theme}
|
||||
getItem={this.getItem}
|
||||
getItemCount={this.getItemCount}
|
||||
contentContainerStyle={styles.postListContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
postListContent: {
|
||||
paddingTop: 5
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
@@ -13,6 +14,7 @@ import PostProfilePicture from './post_profile_picture';
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {config} = state.entities.general;
|
||||
const post = getPost(state, ownProps.postId);
|
||||
const user = getUser(state, post.user_id);
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
@@ -20,7 +22,7 @@ function mapStateToProps(state, ownProps) {
|
||||
fromWebHook: post.props && post.props.from_webhook === 'true',
|
||||
isSystemMessage: isSystemMessage(post),
|
||||
overrideIconUrl: post.props && post.props.override_icon_url,
|
||||
userId: post.user_id,
|
||||
user,
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ function PostProfilePicture(props) {
|
||||
onViewUserProfile,
|
||||
overrideIconUrl,
|
||||
theme,
|
||||
userId
|
||||
user
|
||||
} = props;
|
||||
if (isSystemMessage) {
|
||||
return (
|
||||
@@ -51,7 +51,7 @@ function PostProfilePicture(props) {
|
||||
|
||||
return (
|
||||
<ProfilePicture
|
||||
userId={userId}
|
||||
user={user}
|
||||
size={PROFILE_PICTURE_SIZE}
|
||||
/>
|
||||
);
|
||||
@@ -60,7 +60,7 @@ function PostProfilePicture(props) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onViewUserProfile}>
|
||||
<ProfilePicture
|
||||
userId={userId}
|
||||
user={user}
|
||||
size={PROFILE_PICTURE_SIZE}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
@@ -74,7 +74,7 @@ PostProfilePicture.propTypes = {
|
||||
overrideIconUrl: PropTypes.string,
|
||||
onViewUserProfile: PropTypes.func,
|
||||
theme: PropTypes.object,
|
||||
userId: PropTypes.string
|
||||
user: PropTypes.object
|
||||
};
|
||||
|
||||
PostProfilePicture.defaultProps = {
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Ionicons';
|
||||
import ImagePicker from 'react-native-image-picker';
|
||||
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
export default class AttachmentButton extends PureComponent {
|
||||
static propTypes = {
|
||||
blurTextBox: PropTypes.func.isRequired,
|
||||
navigator: PropTypes.object.isRequired,
|
||||
theme: PropTypes.object.isRequired,
|
||||
uploadFiles: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
attachFileFromCamera = () => {
|
||||
const options = {
|
||||
quality: 0.7,
|
||||
noData: true,
|
||||
storageOptions: {
|
||||
cameraRoll: true,
|
||||
waitUntilSaved: true
|
||||
}
|
||||
};
|
||||
|
||||
ImagePicker.launchCamera(options, (response) => {
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadFiles([response]);
|
||||
});
|
||||
};
|
||||
|
||||
attachFileFromLibrary = () => {
|
||||
const options = {
|
||||
quality: 0.7,
|
||||
noData: true
|
||||
};
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
options.mediaType = 'mixed';
|
||||
}
|
||||
|
||||
ImagePicker.launchImageLibrary(options, (response) => {
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadFiles([response]);
|
||||
});
|
||||
};
|
||||
|
||||
attachVideoFromLibraryAndroid = () => {
|
||||
const options = {
|
||||
quality: 0.7,
|
||||
mediaType: 'video',
|
||||
noData: true
|
||||
};
|
||||
|
||||
ImagePicker.launchImageLibrary(options, (response) => {
|
||||
if (response.error || response.didCancel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uploadFiles([response]);
|
||||
});
|
||||
}
|
||||
|
||||
uploadFiles = (images) => {
|
||||
this.props.uploadFiles(images);
|
||||
};
|
||||
|
||||
handleFileAttachmentOption = (action) => {
|
||||
this.props.navigator.dismissModal({
|
||||
animationType: 'none'
|
||||
});
|
||||
|
||||
// Have to wait to launch the library attachment action.
|
||||
// If we call the action after dismissModal with no delay then the
|
||||
// Wix navigator will dismiss the library attachment modal as well.
|
||||
setTimeout(() => {
|
||||
if (typeof action === 'function') {
|
||||
action();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
showFileAttachmentOptions = () => {
|
||||
this.props.blurTextBox();
|
||||
const options = {
|
||||
items: [{
|
||||
action: () => this.handleFileAttachmentOption(this.attachFileFromCamera),
|
||||
text: {
|
||||
id: 'mobile.file_upload.camera',
|
||||
defaultMessage: 'Take Photo or Video'
|
||||
},
|
||||
icon: 'camera'
|
||||
}, {
|
||||
action: () => this.handleFileAttachmentOption(this.attachFileFromLibrary),
|
||||
text: {
|
||||
id: 'mobile.file_upload.library',
|
||||
defaultMessage: 'Photo Library'
|
||||
},
|
||||
icon: 'photo'
|
||||
}]
|
||||
};
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
options.items.push({
|
||||
action: () => this.handleFileAttachmentOption(this.attachVideoFromLibraryAndroid),
|
||||
text: {
|
||||
id: 'mobile.file_upload.video',
|
||||
defaultMessage: 'Video Library'
|
||||
},
|
||||
icon: 'file-video-o'
|
||||
});
|
||||
}
|
||||
|
||||
this.props.navigator.showModal({
|
||||
screen: 'OptionsModal',
|
||||
title: '',
|
||||
animationType: 'none',
|
||||
passProps: {
|
||||
items: options.items
|
||||
},
|
||||
navigatorStyle: {
|
||||
navBarHidden: true,
|
||||
statusBarHidden: false,
|
||||
statusBarHideWithNavBar: false,
|
||||
screenBackgroundColor: 'transparent',
|
||||
modalPresentationStyle: 'overCurrentContext'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={this.showFileAttachmentOptions}
|
||||
style={style.buttonContainer}
|
||||
>
|
||||
<Icon
|
||||
size={30}
|
||||
style={style.attachIcon}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.9)}
|
||||
name='md-add'
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
attachIcon: {
|
||||
marginTop: Platform.select({
|
||||
ios: 2,
|
||||
android: 0
|
||||
})
|
||||
},
|
||||
buttonContainer: {
|
||||
height: Platform.select({
|
||||
ios: 34,
|
||||
android: 36
|
||||
}),
|
||||
width: 45,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
});
|
||||
@@ -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 {getUsersTyping} from 'mattermost-redux/selectors/entities/typing';
|
||||
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import Typing from './typing';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
typing: getUsersTyping(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Typing);
|
||||
@@ -1,102 +0,0 @@
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Animated,
|
||||
Text
|
||||
} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import {makeStyleSheetFromTheme} from 'app/utils/theme';
|
||||
|
||||
const {View: AnimatedView} = Animated;
|
||||
|
||||
export default class Typing extends PureComponent {
|
||||
static propTypes = {
|
||||
theme: PropTypes.object.isRequired,
|
||||
typing: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
state = {
|
||||
typingHeight: new Animated.Value(0)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.typing.length && !this.props.typing.length) {
|
||||
this.animateTyping(true);
|
||||
} else if (!nextProps.typing.length) {
|
||||
this.animateTyping();
|
||||
}
|
||||
}
|
||||
|
||||
animateTyping = (show = false) => {
|
||||
const height = show ? 20 : 0;
|
||||
|
||||
Animated.timing(this.state.typingHeight, {
|
||||
toValue: height,
|
||||
duration: 200
|
||||
}).start();
|
||||
}
|
||||
|
||||
renderTyping = () => {
|
||||
const {typing} = this.props;
|
||||
const nextTyping = [...typing];
|
||||
const numUsers = nextTyping.length;
|
||||
|
||||
switch (numUsers) {
|
||||
case 0:
|
||||
return null;
|
||||
case 1:
|
||||
return (
|
||||
<FormattedText
|
||||
id='msg_typing.isTyping'
|
||||
defaultMessage='{user} is typing...'
|
||||
values={{
|
||||
user: nextTyping[0]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
default: {
|
||||
const last = nextTyping.pop();
|
||||
return (
|
||||
<FormattedText
|
||||
id='msg_typing.areTyping'
|
||||
defaultMessage='{users} and {last} are typing...'
|
||||
values={{
|
||||
users: (nextTyping.join(', ')),
|
||||
last
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = getStyleSheet(this.props.theme);
|
||||
|
||||
return (
|
||||
<AnimatedView style={{height: this.state.typingHeight}}>
|
||||
<Text
|
||||
style={style.typing}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
{this.renderTyping()}
|
||||
</Text>
|
||||
</AnimatedView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
return {
|
||||
typing: {
|
||||
paddingLeft: 10,
|
||||
paddingTop: 3,
|
||||
fontSize: 11,
|
||||
marginBottom: 5,
|
||||
color: theme.centerChannelColor,
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -3,43 +3,34 @@
|
||||
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {createPost} from 'mattermost-redux/actions/posts';
|
||||
import {userTyping} from 'mattermost-redux/actions/websocket';
|
||||
|
||||
import {handleClearFiles, handleRemoveLastFile, handleUploadFiles} from 'app/actions/views/file_upload';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
import {canUploadFilesOnMobile} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {addReactionToLatestPost} from 'app/actions/views/emoji';
|
||||
import {handlePostDraftChanged} from 'app/actions/views/channel';
|
||||
import {handleClearFiles, handleRemoveLastFile, handleUploadFiles} from 'app/actions/views/file_upload';
|
||||
import {handleCommentDraftChanged} from 'app/actions/views/thread';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
import {getCurrentChannelDraft, getThreadDraft} from 'app/selectors/views';
|
||||
import {getUsersTyping} from 'mattermost-redux/selectors/entities/typing';
|
||||
|
||||
import PostTextbox from './post_textbox';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const currentDraft = ownProps.rootId ? getThreadDraft(state, ownProps.rootId) : getCurrentChannelDraft(state);
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
canUploadFiles: canUploadFilesOnMobile(state),
|
||||
channelIsLoading: state.views.channel.loading,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
files: currentDraft.files,
|
||||
typing: getUsersTyping(state),
|
||||
theme: getTheme(state),
|
||||
uploadFileRequestStatus: state.requests.files.uploadFiles.status,
|
||||
value: currentDraft.draft
|
||||
uploadFileRequestStatus: state.requests.files.uploadFiles.status
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
addReactionToLatestPost,
|
||||
createPost,
|
||||
handleClearFiles,
|
||||
handleCommentDraftChanged,
|
||||
handlePostDraftChanged,
|
||||
handleRemoveLastFile,
|
||||
handleUploadFiles,
|
||||
userTyping
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user