Compare commits
16 Commits
match
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d451cf6bde | ||
|
|
38fa757942 | ||
|
|
ad249e9fe4 | ||
|
|
44708f5bba | ||
|
|
ca308dee84 | ||
|
|
d640a5643d | ||
|
|
da575c7cb1 | ||
|
|
962a1759a5 | ||
|
|
50f616f5d7 | ||
|
|
1a20677eba | ||
|
|
5b5b7bc620 | ||
|
|
456ab53ecf | ||
|
|
9c87835802 | ||
|
|
11cc2e57f5 | ||
|
|
ac5914ef15 | ||
|
|
9c15ae1c5b |
@@ -14,7 +14,7 @@ executors:
|
||||
NODE_ENV: production
|
||||
BABEL_ENV: production
|
||||
docker:
|
||||
- image: circleci/android:api-30-node
|
||||
- image: circleci/android:api-29-node
|
||||
working_directory: ~/mattermost-mobile
|
||||
resource_class: <<parameters.resource_class>>
|
||||
|
||||
@@ -24,7 +24,7 @@ executors:
|
||||
NODE_ENV: production
|
||||
BABEL_ENV: production
|
||||
macos:
|
||||
xcode: "13.0.0"
|
||||
xcode: "12.1.0"
|
||||
working_directory: ~/mattermost-mobile
|
||||
shell: /bin/bash --login -o pipefail
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"mattermost",
|
||||
"import"
|
||||
"mattermost"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
@@ -48,39 +47,7 @@
|
||||
"@typescript-eslint/no-use-before-define": 0,
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/member-delimiter-style": 2,
|
||||
"import/order": [
|
||||
2,
|
||||
{
|
||||
"groups": ["builtin", "external", "parent", "sibling", "index", "type"],
|
||||
"newlines-between": "always",
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "@(@react-native-community|@react-native-cookies|@react-navigation|@rudderstack|@sentry|@testing-library|@storybook)/**",
|
||||
"group": "external",
|
||||
"position": "before"
|
||||
},
|
||||
{
|
||||
"pattern": "@{**,*/**}",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "app/**",
|
||||
"group": "parent",
|
||||
"position": "before"
|
||||
}
|
||||
],
|
||||
"alphabetize": {
|
||||
"order": "asc",
|
||||
"caseInsensitive": true
|
||||
},
|
||||
"pathGroupsExcludedImportTypes": ["type"]
|
||||
}
|
||||
],
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": "error"
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
@@ -23,6 +23,9 @@ node_modules/react-native/flow/
|
||||
[options]
|
||||
emoji=true
|
||||
|
||||
esproposal.optional_chaining=enable
|
||||
esproposal.nullish_coalescing=enable
|
||||
|
||||
exact_by_default=true
|
||||
|
||||
module.file_ext=.js
|
||||
@@ -61,4 +64,4 @@ untyped-import
|
||||
untyped-type-import
|
||||
|
||||
[version]
|
||||
^0.149.0
|
||||
^0.137.0
|
||||
|
||||
106
NOTICE.txt
@@ -148,29 +148,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## @react-native-community/datetimepicker
|
||||
|
||||
This product contains '@react-native-community/datetimepicker' by React Native Community.
|
||||
|
||||
React Native date & time picker component for iOS, Android and Windows.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/react-native-datetimepicker/datetimepicker
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 React Native Community
|
||||
|
||||
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-community/masked-view
|
||||
|
||||
This product contains '@react-native-community/masked-view' by React Native Community.
|
||||
@@ -532,29 +509,6 @@ SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## @types/redux-mock-store
|
||||
|
||||
This product contains '@types/redux-mock-store' by Redux.
|
||||
|
||||
A mock store for testing Redux async action creators and middleware. The mock store will create an array of dispatched actions which serve as an action log for tests.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/reduxjs/redux-mock-store
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Arnaud Benard
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## analytics-react-native
|
||||
|
||||
This product contains a modified version of 'analytics-react-native' by Segment.
|
||||
@@ -576,66 +530,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
||||
|
||||
---
|
||||
|
||||
## array.prototype.flat
|
||||
|
||||
This product contains a modified version of 'array.prototype.flat' by ECMAScript Shims.
|
||||
|
||||
An ES2019 spec-compliant Array.prototype.flat shim/polyfill/replacement that works as far down as ES3.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/es-shims/Array.prototype.flat
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 ECMAScript Shims
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## base-64
|
||||
|
||||
This product contains a modified version of 'base-64' by Dan Kogai.
|
||||
|
||||
Yet another Base64 transcoder.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/dankogai/js-base64
|
||||
|
||||
* LICENSE: BSD 3-Clause "New" or "Revised" License
|
||||
|
||||
Copyright (c) 2014, Dan Kogai All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
Neither the name of {{{project}}} nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
---
|
||||
|
||||
## commonmark
|
||||
|
||||
This product contains a modified version of 'commonmark' by John MacFarlane.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Mattermost Mobile
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (5.37.0)
|
||||
- **Minimum Server versions:** Current ESR version (5.31.3)
|
||||
- **Supported iOS versions:** 11+
|
||||
- **Supported Android versions:** 7.0+
|
||||
|
||||
|
||||
@@ -122,13 +122,18 @@ def enableHermes = project.ext.react.get("enableHermes", false);
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.mattermost.rnbeta"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
||||
versionCode 370
|
||||
versionName "1.47.0"
|
||||
versionCode 364
|
||||
versionName "1.45.1"
|
||||
multiDexEnabled = true
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
@@ -264,7 +269,7 @@ dependencies {
|
||||
// Run this once to be able to run the application with BUCK
|
||||
// puts all compile dependencies into folder libs for BUCK to use
|
||||
task copyDownloadableDepsToLibs(type: Copy) {
|
||||
from configurations.implementation
|
||||
from configurations.compile
|
||||
into 'libs'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -35,7 +31,6 @@ import org.json.JSONObject;
|
||||
|
||||
public class CustomPushNotification extends PushNotification {
|
||||
private static final String PUSH_NOTIFICATIONS = "PUSH_NOTIFICATIONS";
|
||||
private static final String VERSION_PREFERENCE = "VERSION_PREFERENCE";
|
||||
private static final String PUSH_TYPE_MESSAGE = "message";
|
||||
private static final String PUSH_TYPE_CLEAR = "clear";
|
||||
private static final String PUSH_TYPE_SESSION = "session";
|
||||
@@ -44,29 +39,6 @@ public class CustomPushNotification extends PushNotification {
|
||||
public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) {
|
||||
super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper);
|
||||
CustomPushNotificationHelper.createNotificationChannels(context);
|
||||
|
||||
try {
|
||||
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
|
||||
String version = String.valueOf(pInfo.versionCode);
|
||||
String storedVersion = null;
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(VERSION_PREFERENCE, Context.MODE_PRIVATE);
|
||||
if (pSharedPref != null) {
|
||||
storedVersion = pSharedPref.getString("Version", "");
|
||||
}
|
||||
|
||||
if (!version.equals(storedVersion)) {
|
||||
if (pSharedPref != null) {
|
||||
SharedPreferences.Editor editor = pSharedPref.edit();
|
||||
editor.putString("Version", version);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
Map<String, List<Integer>> inputMap = new HashMap<>();
|
||||
saveNotificationsMap(context, inputMap);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void cancelNotification(Context context, String channelId, Integer notificationId) {
|
||||
@@ -77,23 +49,10 @@ public class CustomPushNotification extends PushNotification {
|
||||
return;
|
||||
}
|
||||
|
||||
final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
notificationManager.cancel(notificationId);
|
||||
notifications.remove(notificationId);
|
||||
final StatusBarNotification[] statusNotifications = notificationManager.getActiveNotifications();
|
||||
boolean hasMore = false;
|
||||
for (final StatusBarNotification status : statusNotifications) {
|
||||
if (status.getNotification().extras.getString("channel_id").equals(channelId)) {
|
||||
hasMore = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMore) {
|
||||
notificationsInChannel.remove(channelId);
|
||||
}
|
||||
|
||||
saveNotificationsMap(context, notificationsInChannel);
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
notificationManager.cancel(notificationId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,9 +119,8 @@ public class CustomPushNotification extends PushNotification {
|
||||
switch (type) {
|
||||
case PUSH_TYPE_MESSAGE:
|
||||
case PUSH_TYPE_SESSION:
|
||||
boolean createSummary = type.equals(PUSH_TYPE_MESSAGE);
|
||||
if (!mAppLifecycleFacade.isAppVisible()) {
|
||||
boolean createSummary = type.equals(PUSH_TYPE_MESSAGE);
|
||||
|
||||
if (type.equals(PUSH_TYPE_MESSAGE)) {
|
||||
if (channelId != null) {
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
|
||||
@@ -187,6 +145,8 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
|
||||
buildNotification(notificationId, createSummary);
|
||||
} else {
|
||||
notifyReceivedToJS();
|
||||
}
|
||||
break;
|
||||
case PUSH_TYPE_CLEAR:
|
||||
|
||||
@@ -80,10 +80,7 @@ public class CustomPushNotificationHelper {
|
||||
.setName(senderName);
|
||||
|
||||
try {
|
||||
Bitmap avatar = userAvatar(context, senderId);
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
}
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, senderId))));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -245,10 +242,7 @@ public class CustomPushNotificationHelper {
|
||||
.setName("Me");
|
||||
|
||||
try {
|
||||
Bitmap avatar = userAvatar(context, "me");
|
||||
if (avatar != null) {
|
||||
sender.setIcon(IconCompat.createWithBitmap(avatar));
|
||||
}
|
||||
sender.setIcon(IconCompat.createWithBitmap(Objects.requireNonNull(userAvatar(context, "me"))));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -399,10 +393,7 @@ public class CustomPushNotificationHelper {
|
||||
if (channelName.equals(senderName)) {
|
||||
try {
|
||||
String senderId = bundle.getString("sender_id");
|
||||
Bitmap avatar = userAvatar(context, senderId);
|
||||
if (avatar != null) {
|
||||
notification.setLargeIcon(avatar);
|
||||
}
|
||||
notification.setLargeIcon(userAvatar(context, senderId));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ private final ReactNativeHost mReactNativeHost =
|
||||
// Packages that cannot be auto linked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
packages.add(new RNNotificationsPackage(MainApplication.this));
|
||||
packages.add(new RNPasteableTextInputPackage());
|
||||
packages.add(
|
||||
new TurboReactPackage() {
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
public interface RNEditTextOnPasteListener {
|
||||
void onPaste(Uri itemUri);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
|
||||
public class RNPasteableActionCallback implements ActionMode.Callback {
|
||||
|
||||
private final RNPasteableEditText mEditText;
|
||||
|
||||
RNPasteableActionCallback(RNPasteableEditText editText) {
|
||||
mEditText = editText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
Bundle config = MainApplication.instance.getManagedConfig();
|
||||
if (config != null) {
|
||||
WritableMap result = Arguments.fromBundle(config);
|
||||
String copyPasteProtection = result.getString("copyAndPasteProtection");
|
||||
assert copyPasteProtection != null;
|
||||
if (copyPasteProtection.equals("true")) {
|
||||
disableMenus(menu);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
Uri uri = this.getUriInClipboard();
|
||||
if (item.getItemId() == android.R.id.paste && uri != null) {
|
||||
mEditText.getOnPasteListener().onPaste(uri);
|
||||
mode.finish();
|
||||
} else {
|
||||
mEditText.onTextContextMenuItem(item.getItemId());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
|
||||
}
|
||||
|
||||
private void disableMenus(Menu menu) {
|
||||
for (int i = 0; i < menu.size(); i++) {
|
||||
MenuItem item = menu.getItem(i);
|
||||
int id = item.getItemId();
|
||||
boolean shouldDisableMenu = (
|
||||
id == android.R.id.paste
|
||||
|| id == android.R.id.copy
|
||||
|| id == android.R.id.cut
|
||||
);
|
||||
item.setEnabled(!shouldDisableMenu);
|
||||
}
|
||||
}
|
||||
|
||||
private Uri getUriInClipboard() {
|
||||
ClipboardManager clipboardManager = (ClipboardManager) mEditText.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboardManager.getPrimaryClip();
|
||||
if (clipData == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ClipData.Item item = clipData.getItemAt(0);
|
||||
if (item == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CharSequence chars = item.getText();
|
||||
if (chars == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String text = chars.toString();
|
||||
if (text.length() > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return item.getUri();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.facebook.react.views.textinput.ReactEditText;
|
||||
|
||||
public class RNPasteableEditText extends ReactEditText {
|
||||
|
||||
private RNEditTextOnPasteListener mOnPasteListener;
|
||||
|
||||
public RNPasteableEditText(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public void setOnPasteListener(RNEditTextOnPasteListener listener) {
|
||||
mOnPasteListener = listener;
|
||||
}
|
||||
|
||||
public RNEditTextOnPasteListener getOnPasteListener() {
|
||||
return mOnPasteListener;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Patterns;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
import com.mattermost.share.RealPathUtil;
|
||||
import com.mattermost.share.ShareModule;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.File;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
public class RNPasteableEditTextOnPasteListener implements RNEditTextOnPasteListener {
|
||||
|
||||
private final RNPasteableEditText mEditText;
|
||||
|
||||
RNPasteableEditTextOnPasteListener(RNPasteableEditText editText) {
|
||||
mEditText = editText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPaste(Uri itemUri) {
|
||||
ReactContext reactContext = (ReactContext)mEditText.getContext();
|
||||
String uri = itemUri.toString();
|
||||
|
||||
WritableArray images = null;
|
||||
WritableMap error = null;
|
||||
|
||||
String uriMimeType = reactContext.getContentResolver().getType(itemUri);
|
||||
if (uriMimeType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Special handle for Google docs
|
||||
if (uri.equals("content://com.google.android.apps.docs.editors.kix.editors.clipboard")) {
|
||||
ClipboardManager clipboardManager = (ClipboardManager) reactContext.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clipData = clipboardManager.getPrimaryClip();
|
||||
if (clipData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClipData.Item item = clipData.getItemAt(0);
|
||||
String htmlText = item.getHtmlText();
|
||||
// Find uri from html
|
||||
Matcher matcher = Patterns.WEB_URL.matcher(htmlText);
|
||||
if (matcher.find()) {
|
||||
uri = htmlText.substring(matcher.start(1), matcher.end());
|
||||
}
|
||||
}
|
||||
|
||||
if (uri.startsWith("http")) {
|
||||
Thread pastImageFromUrlThread = new Thread(new RNPasteableImageFromUrl(reactContext, mEditText, uri));
|
||||
pastImageFromUrlThread.start();
|
||||
return;
|
||||
}
|
||||
|
||||
uri = RealPathUtil.getRealPathFromURI(reactContext, itemUri);
|
||||
if (uri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get type
|
||||
String extension = MimeTypeMap.getFileExtensionFromUrl(uri);
|
||||
if (extension == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
if (mimeType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get fileName
|
||||
String fileName = URLUtil.guessFileName(uri, null, mimeType);
|
||||
|
||||
if (uri.contains(ShareModule.CACHE_DIR_NAME) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
uri = moveToImagesCache(uri, fileName);
|
||||
}
|
||||
|
||||
if (uri == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get fileSize
|
||||
long fileSize;
|
||||
try {
|
||||
ContentResolver contentResolver = reactContext.getContentResolver();
|
||||
AssetFileDescriptor assetFileDescriptor = contentResolver.openAssetFileDescriptor(itemUri, "r");
|
||||
if (assetFileDescriptor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
fileSize = assetFileDescriptor.getLength();
|
||||
|
||||
WritableMap image = Arguments.createMap();
|
||||
image.putString("type", mimeType);
|
||||
image.putDouble("fileSize", fileSize);
|
||||
image.putString("fileName", fileName);
|
||||
image.putString("uri", "file://" + uri);
|
||||
|
||||
images = Arguments.createArray();
|
||||
images.pushMap(image);
|
||||
} catch (FileNotFoundException e) {
|
||||
error = Arguments.createMap();
|
||||
error.putString("message", e.getMessage());
|
||||
}
|
||||
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putArray("data", images);
|
||||
event.putMap("error", error);
|
||||
|
||||
reactContext
|
||||
.getJSModule(RCTEventEmitter.class)
|
||||
.receiveEvent(
|
||||
mEditText.getId(),
|
||||
"onPaste",
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
private String moveToImagesCache(String src, String fileName) {
|
||||
ReactContext ctx = (ReactContext)mEditText.getContext();
|
||||
String cacheFolder = ctx.getCacheDir().getAbsolutePath() + "/Images/";
|
||||
String dest = cacheFolder + fileName;
|
||||
File folder = new File(cacheFolder);
|
||||
|
||||
try {
|
||||
if (!folder.exists()) {
|
||||
boolean created = folder.mkdirs();
|
||||
if (!created) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Files.move(Paths.get(src), Paths.get(dest));
|
||||
} catch (FileAlreadyExistsException fileError) {
|
||||
// Do nothing and return dest path
|
||||
} catch (Exception err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dest;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.uimanager.events.RCTEventEmitter;
|
||||
import com.facebook.react.views.textinput.ReactEditText;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
public class RNPasteableImageFromUrl implements Runnable {
|
||||
|
||||
private final ReactContext mContext;
|
||||
private final String mUri;
|
||||
private final ReactEditText mTarget;
|
||||
|
||||
RNPasteableImageFromUrl(ReactContext context, ReactEditText target, String uri) {
|
||||
mContext = context;
|
||||
mUri = uri;
|
||||
mTarget = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
WritableArray images = null;
|
||||
WritableMap error = null;
|
||||
|
||||
try {
|
||||
URL url = new URL(mUri);
|
||||
URLConnection u = url.openConnection();
|
||||
|
||||
// Get type
|
||||
String mimeType = u.getHeaderField("Content-Type");
|
||||
if (!mimeType.startsWith("image")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get fileSize
|
||||
long fileSize = Long.parseLong(u.getHeaderField("Content-Length"));
|
||||
|
||||
// Get fileName
|
||||
String contentDisposition = u.getHeaderField("Content-Disposition");
|
||||
int startIndex = contentDisposition.indexOf("filename=\"") + 10;
|
||||
int endIndex = contentDisposition.length() - 1;
|
||||
String fileName = contentDisposition.substring(startIndex, endIndex);
|
||||
|
||||
WritableMap image = Arguments.createMap();
|
||||
image.putString("type", mimeType);
|
||||
image.putDouble("fileSize", fileSize);
|
||||
image.putString("fileName", fileName);
|
||||
image.putString("uri", mUri);
|
||||
|
||||
images = Arguments.createArray();
|
||||
images.pushMap(image);
|
||||
|
||||
} catch (IOException e) {
|
||||
error = Arguments.createMap();
|
||||
error.putString("message", e.getMessage());
|
||||
}
|
||||
|
||||
WritableMap event = Arguments.createMap();
|
||||
event.putArray("data", images);
|
||||
event.putMap("error", error);
|
||||
|
||||
mContext
|
||||
.getJSModule(RCTEventEmitter.class)
|
||||
.receiveEvent(
|
||||
mTarget.getId(),
|
||||
"onPaste",
|
||||
event
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat;
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
||||
import android.text.InputType;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
import com.facebook.react.common.MapBuilder;
|
||||
import com.facebook.react.uimanager.ThemedReactContext;
|
||||
import com.facebook.react.views.textinput.ReactEditText;
|
||||
import com.facebook.react.views.textinput.ReactTextInputManager;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class RNPasteableTextInputManager extends ReactTextInputManager {
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String getName() {
|
||||
return "PasteableTextInputAndroid";
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public ReactEditText createViewInstance(ThemedReactContext context) {
|
||||
RNPasteableEditText editText = new RNPasteableEditText(context) {
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
|
||||
final InputConnection ic = super.onCreateInputConnection(editorInfo);
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo,
|
||||
new String [] {"image/*"});
|
||||
|
||||
|
||||
final InputConnectionCompat.OnCommitContentListener callback =
|
||||
(inputContentInfo, flags, opts) -> {
|
||||
// read and display inputContentInfo asynchronously
|
||||
if ((flags &
|
||||
InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
|
||||
try {
|
||||
inputContentInfo.requestPermission();
|
||||
}
|
||||
catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.getOnPasteListener().onPaste(inputContentInfo.getContentUri());
|
||||
return true;
|
||||
};
|
||||
return InputConnectionCompat.createWrapper(ic, editorInfo, callback);
|
||||
}
|
||||
};
|
||||
int inputType = editText.getInputType();
|
||||
editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
|
||||
editText.setReturnKeyType("done");
|
||||
editText.setCustomInsertionActionModeCallback(new RNPasteableActionCallback(editText));
|
||||
editText.setCustomSelectionActionModeCallback(new RNPasteableActionCallback(editText));
|
||||
return editText;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addEventEmitters(ThemedReactContext reactContext, ReactEditText editText) {
|
||||
super.addEventEmitters(reactContext, editText);
|
||||
|
||||
RNPasteableEditText pasteableEditText = (RNPasteableEditText)editText;
|
||||
pasteableEditText.setOnPasteListener(new RNPasteableEditTextOnPasteListener(pasteableEditText));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
|
||||
Map<String, Object> map = super.getExportedCustomBubblingEventTypeConstants();
|
||||
assert map != null;
|
||||
map.put(
|
||||
"onPaste",
|
||||
MapBuilder.of(
|
||||
"phasedRegistrationNames",
|
||||
MapBuilder.of("bubbled", "onPaste")));
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class RNPasteableTextInputPackage implements ReactPackage {
|
||||
@Nonnull
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
|
||||
return Arrays.asList(
|
||||
new RNPasteableTextInputManager()
|
||||
);
|
||||
}
|
||||
}
|
||||
BIN
android/app/src/main/res/drawable-hdpi/splash.png
Normal file → Executable file
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 787 KiB |
BIN
android/app/src/main/res/drawable-mdpi/splash.png
Normal file → Executable file
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 413 B |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 348 B |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 610 B |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 833 B |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.0 MiB |
BIN
android/app/src/main/res/drawable-xhdpi/splash.png
Normal file → Executable file
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
BIN
android/app/src/main/res/drawable-xxhdpi/splash.png
Normal file → Executable file
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/splash.png
Normal file → Executable file
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
@@ -1,32 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/splashscreen_bg"
|
||||
android:orientation="vertical">
|
||||
android:background="#ffffff"
|
||||
android:gravity="center_horizontal"
|
||||
tools:context=".SplashScreenActivity">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/splashBackgroundImage"
|
||||
android:id="@+id/imgLogo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/splash_background"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/splashIcon"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:contentDescription="@string/app_name"
|
||||
android:src="@drawable/splash"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
android:adjustViewBounds="true"
|
||||
android:src="@drawable/splash" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file → Executable file
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 855 B |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 638 B After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file → Executable file
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 490 B |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 481 B After Width: | Height: | Size: 847 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file → Executable file
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 985 B |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 847 B After Width: | Height: | Size: 1.2 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file → Executable file
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file → Executable file
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="transparent">#00000000</color>
|
||||
<color name="splashscreen_bg">#1E325C</color>
|
||||
</resources>
|
||||
@@ -2,5 +2,4 @@
|
||||
<resources>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="transparent">#00000000</color>
|
||||
<color name="splashscreen_bg">#FFFFFF</color>
|
||||
</resources>
|
||||
</resources>
|
||||
@@ -3,8 +3,8 @@
|
||||
<!-- Base application theme. -->
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:windowBackground">@color/splashscreen_bg</item>
|
||||
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "30.0.2"
|
||||
buildToolsVersion = "29.0.3"
|
||||
minSdkVersion = 24
|
||||
compileSdkVersion = 30
|
||||
targetSdkVersion = 30
|
||||
compileSdkVersion = 29
|
||||
targetSdkVersion = 29
|
||||
supportLibVersion = "28.0.0"
|
||||
kotlinVersion = "1.5.30"
|
||||
kotlinVersion = "1.3.61"
|
||||
firebaseVersion = "21.0.0"
|
||||
RNNKotlinVersion = kotlinVersion
|
||||
ndkVersion = "20.1.5948944"
|
||||
ndkVersion = "21.1.6352462"
|
||||
|
||||
}
|
||||
repositories {
|
||||
@@ -20,7 +20,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.2.1'
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
|
||||
|
||||
@@ -30,4 +30,4 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.93.0
|
||||
FLIPPER_VERSION=0.75.1
|
||||
|
||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {sendEphemeralPost} from '@actions/views/post';
|
||||
import {Client4} from '@client/rest';
|
||||
import {AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
|
||||
import {ActionFunc, DispatchFunc} from '@mm-redux/types/actions';
|
||||
import {AppCallResponse, AppCallRequest, AppCallType, AppContext} from '@mm-redux/types/apps';
|
||||
import {CommandArgs} from '@mm-redux/types/integrations';
|
||||
import {AppCallResponse, AppForm, AppCallRequest, AppCallType, AppContext} from '@mm-redux/types/apps';
|
||||
import {Post} from '@mm-redux/types/posts';
|
||||
import {cleanForm, makeCallErrorResponse} from '@utils/apps';
|
||||
|
||||
import {AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
|
||||
import {handleGotoLocation} from '@mm-redux/actions/integrations';
|
||||
import {showModal} from './navigation';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {makeCallErrorResponse} from '@utils/apps';
|
||||
import {sendEphemeralPost} from '@actions/views/post';
|
||||
import {CommandArgs} from '@mm-redux/types/integrations';
|
||||
|
||||
export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType, intl: any): ActionFunc {
|
||||
return async () => {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
const res = await Client4.executeAppCall(call, type) as AppCallResponse<Res>;
|
||||
const responseType = res.type || AppCallResponseTypes.OK;
|
||||
@@ -30,7 +38,10 @@ export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType,
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
}
|
||||
|
||||
cleanForm(res.form);
|
||||
const screen = EphemeralStore.getNavigationTopComponentId();
|
||||
if (type === AppCallTypes.SUBMIT && screen !== 'AppForm') {
|
||||
showAppForm(res.form, call, getTheme(getState()));
|
||||
}
|
||||
|
||||
return {data: res};
|
||||
}
|
||||
@@ -51,6 +62,8 @@ export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType,
|
||||
return {error: makeCallErrorResponse(errMsg)};
|
||||
}
|
||||
|
||||
dispatch(handleGotoLocation(res.navigate_to_url, intl));
|
||||
|
||||
return {data: res};
|
||||
default: {
|
||||
const errMsg = intl.formatMessage({
|
||||
@@ -72,6 +85,41 @@ export function doAppCall<Res=unknown>(call: AppCallRequest, type: AppCallType,
|
||||
};
|
||||
}
|
||||
|
||||
const showAppForm = async (form: AppForm, call: AppCallRequest, theme: Theme) => {
|
||||
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
|
||||
let submitButtons = [{
|
||||
id: 'submit-form',
|
||||
showAsAction: 'always',
|
||||
text: 'Submit',
|
||||
}];
|
||||
if (form.submit_buttons) {
|
||||
const options = form.fields.find((f) => f.name === form.submit_buttons)?.options;
|
||||
const newButtons = options?.map((o) => {
|
||||
return {
|
||||
id: 'submit-form_' + o.value,
|
||||
showAsAction: 'always',
|
||||
text: o.label,
|
||||
};
|
||||
});
|
||||
if (newButtons && newButtons.length > 0) {
|
||||
submitButtons = newButtons;
|
||||
}
|
||||
}
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-dialog',
|
||||
icon: closeButton,
|
||||
}],
|
||||
rightButtons: submitButtons,
|
||||
},
|
||||
};
|
||||
|
||||
const passProps = {form, call};
|
||||
showModal('AppForm', form.title, passProps, options);
|
||||
};
|
||||
|
||||
export function postEphemeralCallResponseForPost(response: AppCallResponse, message: string, post: Post): ActionFunc {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
return dispatch(sendEphemeralPost(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeviceTypes} from '@constants';
|
||||
import {DeviceTypes} from 'app/constants';
|
||||
|
||||
export function connection(isOnline) {
|
||||
return async (dispatch, getState) => {
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
/* eslint-disable no-import-assign */
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {PreferenceTypes} from '@mm-redux/action_types';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {PreferenceTypes} from '@mm-redux/action_types';
|
||||
|
||||
import * as CommonSelectors from '@mm-redux/selectors/entities/common';
|
||||
import * as PreferenceSelectors from '@mm-redux/selectors/entities/preferences';
|
||||
import * as PreferenceUtils from '@mm-redux/utils/preference_utils';
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {ChannelTypes, PreferenceTypes, RoleTypes, UserTypes} from '@mm-redux/action_types';
|
||||
import {Client4} from '@client/rest';
|
||||
import {General, Preferences} from '@mm-redux/constants';
|
||||
import {getCurrentChannelId, getRedirectChannelNameForTeam, getChannelsNameMapInTeam} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getMyPreferences} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUsers, getUserIdsInChannels} from '@mm-redux/selectors/entities/users';
|
||||
import {getChannelByName as selectChannelByName, getUserIdFromChannelName, isAutoClosed} from '@mm-redux/utils/channel_utils';
|
||||
import {getPreferenceKey} from '@mm-redux/utils/preference_utils';
|
||||
|
||||
import {ActionResult, GenericAction} from '@mm-redux/types/actions';
|
||||
import {Channel, ChannelMembership} from '@mm-redux/types/channels';
|
||||
import {PreferenceType} from '@mm-redux/types/preferences';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
import {RelationOneToMany} from '@mm-redux/types/utilities';
|
||||
import {getChannelByName as selectChannelByName, getUserIdFromChannelName, isAutoClosed} from '@mm-redux/utils/channel_utils';
|
||||
import {getPreferenceKey} from '@mm-redux/utils/preference_utils';
|
||||
|
||||
import {isDirectChannelVisible, isGroupChannelVisible} from '@utils/channels';
|
||||
import {buildPreference} from '@utils/preferences';
|
||||
|
||||
@@ -118,7 +120,7 @@ export async function fetchMyChannelMember(channelId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function markChannelAsUnread(state: GlobalState, teamId: string, channelId: string, mentions: Array<string>, isRoot = false): Array<GenericAction> {
|
||||
export function markChannelAsUnread(state: GlobalState, teamId: string, channelId: string, mentions: Array<string>): Array<GenericAction> {
|
||||
const {myMembers} = state.entities.channels;
|
||||
const {currentUserId} = state.entities.users;
|
||||
|
||||
@@ -127,7 +129,6 @@ export function markChannelAsUnread(state: GlobalState, teamId: string, channelI
|
||||
data: {
|
||||
channelId,
|
||||
amount: 1,
|
||||
amountRoot: isRoot ? 1 : 0,
|
||||
},
|
||||
}, {
|
||||
type: ChannelTypes.INCREMENT_UNREAD_MSG_COUNT,
|
||||
@@ -135,7 +136,6 @@ export function markChannelAsUnread(state: GlobalState, teamId: string, channelI
|
||||
teamId,
|
||||
channelId,
|
||||
amount: 1,
|
||||
amountRoot: isRoot ? 1 : 0,
|
||||
onlyMentions: myMembers[channelId] && myMembers[channelId].notify_props &&
|
||||
myMembers[channelId].notify_props.mark_unread === General.MENTION,
|
||||
},
|
||||
@@ -148,7 +148,6 @@ export function markChannelAsUnread(state: GlobalState, teamId: string, channelI
|
||||
teamId,
|
||||
channelId,
|
||||
amount: 1,
|
||||
amountRoot: isRoot ? 1 : 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -328,8 +327,7 @@ function fetchDirectMessageProfileIfNeeded(state: GlobalState, channel: Channel,
|
||||
const otherUserId = getUserIdFromChannelName(currentUserId, channel.name);
|
||||
const otherUser = users[otherUserId];
|
||||
const dmVisible = isDirectChannelVisible(currentUserId, myPreferences, channel);
|
||||
const {serverVersion} = state.entities.general;
|
||||
const dmAutoClosed = isAutoClosed(config, myPreferences, channel, channel.last_post_at, otherUser ? otherUser.delete_at : 0, currentChannelId, undefined, serverVersion);
|
||||
const dmAutoClosed = isAutoClosed(config, myPreferences, channel, channel.last_post_at, otherUser ? otherUser.delete_at : 0, currentChannelId);
|
||||
const member = channelMembers.find((cm) => cm.channel_id === channel.id);
|
||||
const dmIsUnread = member ? member.mention_count > 0 : false;
|
||||
const dmFetchProfile = dmIsUnread || (dmVisible && !dmAutoClosed);
|
||||
@@ -356,9 +354,7 @@ function fetchGroupMessageProfilesIfNeeded(state: GlobalState, channel: Channel,
|
||||
const myPreferences = getMyPreferences(state);
|
||||
const config = getConfig(state);
|
||||
const gmVisible = isGroupChannelVisible(myPreferences, channel);
|
||||
const {serverVersion} = state.entities.general;
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const gmAutoClosed = isAutoClosed(config, myPreferences, channel, channel.last_post_at, 0, currentChannelId, undefined, serverVersion);
|
||||
const gmAutoClosed = isAutoClosed(config, myPreferences, channel, channel.last_post_at, 0);
|
||||
const channelMember = channelMembers.find((cm) => cm.channel_id === channel.id);
|
||||
let hasMentions = false;
|
||||
let isUnread = false;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import merge from 'deepmerge';
|
||||
import {Keyboard, Platform} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import merge from 'deepmerge';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {DeviceTypes, NavigationTypes} from '@constants';
|
||||
import {CHANNEL} from '@constants/screen';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import EventEmmiter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {DeviceTypes, NavigationTypes} from '@constants';
|
||||
import {CHANNEL} from '@constants/screen';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import Store from '@store/store';
|
||||
|
||||
@@ -92,7 +92,7 @@ export function resetToChannel(passProps = {}) {
|
||||
}
|
||||
|
||||
export function resetToSelectServer(allowOtherServers) {
|
||||
const theme = Preferences.THEMES.denim;
|
||||
const theme = Preferences.THEMES.default;
|
||||
|
||||
EphemeralStore.clearNavigationComponents();
|
||||
|
||||
@@ -292,7 +292,7 @@ export function showModal(name, title, passProps = {}, options = {}) {
|
||||
}
|
||||
|
||||
export function showModalOverCurrentContext(name, passProps = {}, options = {}) {
|
||||
const title = passProps.title || '';
|
||||
const title = '';
|
||||
|
||||
let animations;
|
||||
switch (Platform.OS) {
|
||||
@@ -318,20 +318,14 @@ export function showModalOverCurrentContext(name, passProps = {}, options = {})
|
||||
default:
|
||||
animations = {
|
||||
showModal: {
|
||||
enter: {
|
||||
enabled: false,
|
||||
},
|
||||
exit: {
|
||||
enabled: false,
|
||||
},
|
||||
enabled: false,
|
||||
enter: {},
|
||||
exit: {},
|
||||
},
|
||||
dismissModal: {
|
||||
enter: {
|
||||
enabled: false,
|
||||
},
|
||||
exit: {
|
||||
enabled: false,
|
||||
},
|
||||
enabled: false,
|
||||
enter: {},
|
||||
exit: {},
|
||||
},
|
||||
};
|
||||
break;
|
||||
@@ -373,40 +367,12 @@ export function showSearchModal(initialValue = '') {
|
||||
showModal(name, title, passProps, options);
|
||||
}
|
||||
|
||||
export const showAppForm = async (form, call, theme) => {
|
||||
const closeButton = await CompassIcon.getImageSource('close', 24, theme.sidebarHeaderTextColor);
|
||||
|
||||
let submitButtons;
|
||||
const customSubmitButtons = form.submit_buttons && form.fields.find((f) => f.name === form.submit_buttons)?.options;
|
||||
|
||||
if (!customSubmitButtons?.length) {
|
||||
submitButtons = [{
|
||||
id: 'submit-form',
|
||||
showAsAction: 'always',
|
||||
text: 'Submit',
|
||||
}];
|
||||
}
|
||||
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-dialog',
|
||||
icon: closeButton,
|
||||
}],
|
||||
rightButtons: submitButtons,
|
||||
},
|
||||
};
|
||||
|
||||
const passProps = {form, call};
|
||||
showModal('AppForm', form.title, passProps, options);
|
||||
};
|
||||
|
||||
export async function dismissModal(options = {}) {
|
||||
if (!EphemeralStore.hasModalsOpened()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentId = options.componentId || EphemeralStore.getNavigationTopComponentId();
|
||||
const componentId = EphemeralStore.getNavigationTopComponentId();
|
||||
|
||||
try {
|
||||
await Navigation.dismissModal(componentId, options);
|
||||
@@ -457,13 +423,6 @@ export function showOverlay(name, passProps, options = {}) {
|
||||
overlay: {
|
||||
interceptTouchOutside: false,
|
||||
},
|
||||
...Platform.select({
|
||||
android: {
|
||||
statusBar: {
|
||||
drawBehind: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
Navigation.showOverlay({
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import merge from 'deepmerge';
|
||||
import {Platform} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import merge from 'deepmerge';
|
||||
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import * as NavigationActions from '@actions/navigation';
|
||||
import {NavigationTypes} from '@constants';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import intitialState from '@store/initial_state';
|
||||
import Store from '@store/store';
|
||||
import {NavigationTypes} from '@constants';
|
||||
|
||||
jest.unmock('@actions/navigation');
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
@@ -37,7 +38,7 @@ describe('@actions/navigation', () => {
|
||||
const topComponentId = 'top-component-id';
|
||||
const name = 'name';
|
||||
const title = 'title';
|
||||
const theme = Preferences.THEMES.denim;
|
||||
const theme = Preferences.THEMES.default;
|
||||
const passProps = {
|
||||
testProp: 'prop',
|
||||
};
|
||||
@@ -313,20 +314,14 @@ describe('@actions/navigation', () => {
|
||||
},
|
||||
animations: {
|
||||
showModal: {
|
||||
enter: {
|
||||
enabled: false,
|
||||
},
|
||||
exit: {
|
||||
enabled: false,
|
||||
},
|
||||
enabled: false,
|
||||
enter: {},
|
||||
exit: {},
|
||||
},
|
||||
dismissModal: {
|
||||
enter: {
|
||||
enabled: false,
|
||||
},
|
||||
exit: {
|
||||
enabled: false,
|
||||
},
|
||||
enabled: false,
|
||||
enter: {},
|
||||
exit: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ViewTypes} from '@constants';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export function dismissBanner(text) {
|
||||
return {
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {lastChannelIdForTeam, loadSidebarDirectMessagesProfiles} from '@actions/helpers/channels';
|
||||
import {getPosts, getPostsBefore, getPostsSince, loadUnreadChannelPosts} from '@actions/views/post';
|
||||
import {Client4} from '@client/rest';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
import {ChannelTypes, RoleTypes, GroupTypes} from '@mm-redux/action_types';
|
||||
import {fetchAppBindings} from '@mm-redux/actions/apps';
|
||||
import {fetchMyCategories} from '@mm-redux/actions/channel_categories';
|
||||
import {
|
||||
fetchMyChannelsAndMembers,
|
||||
getChannelByName,
|
||||
@@ -20,8 +13,12 @@ import {
|
||||
leaveChannel as serviceLeaveChannel,
|
||||
} from '@mm-redux/actions/channels';
|
||||
import {savePreferences} from '@mm-redux/actions/preferences';
|
||||
import {getLicense} from '@mm-redux/selectors/entities/general';
|
||||
import {addUserToTeam, getTeamByName, removeUserFromTeam, selectTeam} from '@mm-redux/actions/teams';
|
||||
import {Client4} from '@client/rest';
|
||||
import {General, Preferences} from '@mm-redux/constants';
|
||||
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {
|
||||
getCurrentChannelId,
|
||||
getRedirectChannelNameForTeam,
|
||||
@@ -29,22 +26,21 @@ import {
|
||||
getMyChannelMemberships,
|
||||
isManuallyUnread,
|
||||
} from '@mm-redux/selectors/entities/channels';
|
||||
import {getLicense} from '@mm-redux/selectors/entities/general';
|
||||
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
||||
import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getTeamByName as selectTeamByName, getCurrentTeam, getTeamMemberships} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {getTeamByName as selectTeamByName, getCurrentTeam, getTeamMemberships} from '@mm-redux/selectors/entities/teams';
|
||||
|
||||
import {getChannelByName as selectChannelByName, getChannelsIdForTeam} from '@mm-redux/utils/channel_utils';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {lastChannelIdForTeam, loadSidebarDirectMessagesProfiles} from '@actions/helpers/channels';
|
||||
import {getPosts, getPostsBefore, getPostsSince, loadUnreadChannelPosts} from '@actions/views/post';
|
||||
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
|
||||
import {getChannelReachable} from '@selectors/channel';
|
||||
import {getViewingGlobalThreads} from '@selectors/threads';
|
||||
import telemetry, {PERF_MARKERS} from '@telemetry';
|
||||
import {appsEnabled} from '@utils/apps';
|
||||
import {shouldShowLegacySidebar} from '@utils/categories';
|
||||
import {isDirectChannelVisible, isGroupChannelVisible, getChannelSinceValue, privateChannelJoinPrompt} from '@utils/channels';
|
||||
import {isPendingPost} from '@utils/general';
|
||||
|
||||
import {handleNotViewingGlobalThreadsScreen} from './threads';
|
||||
import {fetchAppBindings} from '@mm-redux/actions/apps';
|
||||
import {appsEnabled} from '@utils/apps';
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
|
||||
@@ -128,11 +124,9 @@ export function fetchPostActionWithRetry(action, maxTries = MAX_RETRIES) {
|
||||
export function selectInitialChannel(teamId) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
|
||||
if (!collapsedThreadsEnabled || (collapsedThreadsEnabled && !getViewingGlobalThreads(state))) {
|
||||
const channelId = lastChannelIdForTeam(state, teamId);
|
||||
dispatch(handleSelectChannel(channelId));
|
||||
}
|
||||
const channelId = lastChannelIdForTeam(state, teamId);
|
||||
|
||||
dispatch(handleSelectChannel(channelId));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -225,9 +219,6 @@ export function handleSelectChannel(channelId) {
|
||||
teamId: channel.team_id || currentTeamId,
|
||||
},
|
||||
});
|
||||
if (getViewingGlobalThreads(state)) {
|
||||
actions.push(handleNotViewingGlobalThreadsScreen());
|
||||
}
|
||||
|
||||
dispatch(batchActions(actions, 'BATCH_SWITCH_CHANNEL'));
|
||||
|
||||
@@ -385,7 +376,6 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
|
||||
|
||||
if (channel) {
|
||||
const unreadMessageCount = channel.total_msg_count - member.msg_count;
|
||||
const unreadMessageCountRoot = channel.total_msg_count_root - member.msg_count_root;
|
||||
actions.push({
|
||||
type: ChannelTypes.SET_UNREAD_MSG_COUNT,
|
||||
data: {
|
||||
@@ -398,7 +388,6 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
|
||||
teamId: channel.team_id,
|
||||
channelId,
|
||||
amount: unreadMessageCount,
|
||||
amountRoot: unreadMessageCountRoot,
|
||||
},
|
||||
}, {
|
||||
type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
|
||||
@@ -406,7 +395,6 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
|
||||
teamId: channel.team_id,
|
||||
channelId,
|
||||
amount: member.mention_count,
|
||||
amountRoot: member.mention_count_root,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -427,7 +415,6 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
|
||||
teamId: prevChannel.team_id,
|
||||
channelId: prevChannelId,
|
||||
amount: prevChannel.total_msg_count - prevMember.msg_count,
|
||||
amountRoot: prevChannel.total_msg_count_root - prevMember.msg_count_root,
|
||||
},
|
||||
}, {
|
||||
type: ChannelTypes.DECREMENT_UNREAD_MENTION_COUNT,
|
||||
@@ -435,7 +422,6 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
|
||||
teamId: prevChannel.team_id,
|
||||
channelId: prevChannelId,
|
||||
amount: prevMember.mention_count,
|
||||
amountRoot: prevMember.mention_count_root,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -640,10 +626,11 @@ function loadGroupData(isReconnect = false) {
|
||||
const actions = [];
|
||||
const team = getCurrentTeam(state);
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const serverVersion = state.entities.general.serverVersion;
|
||||
const license = getLicense(state);
|
||||
const hasLicense = license?.IsLicensed === 'true' && license?.LDAPGroups === 'true';
|
||||
|
||||
if (hasLicense && team) {
|
||||
if (hasLicense && team && isMinimumServerVersion(serverVersion, 5, 24)) {
|
||||
for (let i = 0; i <= MAX_RETRIES; i++) {
|
||||
try {
|
||||
if (team.group_constrained) {
|
||||
@@ -747,10 +734,6 @@ export function loadChannelsForTeam(teamId, skipDispatch = false, isReconnect =
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldShowLegacySidebar(state)) {
|
||||
await dispatch(fetchMyCategories(teamId));
|
||||
}
|
||||
|
||||
if (data.channels) {
|
||||
actions.push({
|
||||
type: ChannelTypes.RECEIVED_MY_CHANNELS_WITH_MEMBERS,
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import testHelper from 'test/test_helper';
|
||||
|
||||
import * as ChannelActions from '@actions/views/channel';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {ChannelTypes} from '@mm-redux/action_types';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import postReducer from '@mm-redux/reducers/entities/posts';
|
||||
import initialState from '@store/initial_state';
|
||||
import testHelper from '@test/test_helper';
|
||||
import {General} from '@mm-redux/constants';
|
||||
|
||||
const {
|
||||
handleSelectChannel,
|
||||
@@ -166,12 +167,6 @@ describe('Actions.Views.Channel', () => {
|
||||
[currentTeamId]: {},
|
||||
},
|
||||
},
|
||||
general: {
|
||||
config: {
|
||||
EnableLegacySidebar: 'true',
|
||||
},
|
||||
serverVersion: '5.12.0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -180,7 +175,7 @@ describe('Actions.Views.Channel', () => {
|
||||
channelSelectors.getCurrentChannelId = jest.fn(() => currentChannelId);
|
||||
channelSelectors.getMyChannelMember = jest.fn(() => ({data: {member: {}}}));
|
||||
|
||||
const appChannelSelectors = require('@selectors/channel');
|
||||
const appChannelSelectors = require('app/selectors/channel');
|
||||
const getChannelReachableOriginal = appChannelSelectors.getChannelReachable;
|
||||
appChannelSelectors.getChannelReachable = jest.fn(() => true);
|
||||
|
||||
|
||||
@@ -3,16 +3,18 @@
|
||||
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
import {doAppCall, postEphemeralCallResponseForCommandArgs} from '@actions/apps';
|
||||
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
|
||||
import {IntegrationTypes} from '@mm-redux/action_types';
|
||||
import {executeCommand as executeCommandService} from '@mm-redux/actions/integrations';
|
||||
import {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {AppCallResponseTypes, AppCallTypes} from '@mm-redux/constants/apps';
|
||||
import {DispatchFunc, GetStateFunc, ActionFunc} from '@mm-redux/types/actions';
|
||||
import {AppCallResponse} from '@mm-redux/types/apps';
|
||||
import {CommandArgs} from '@mm-redux/types/integrations';
|
||||
|
||||
import {AppCommandParser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser';
|
||||
|
||||
import {doAppCall, postEphemeralCallResponseForCommandArgs} from '@actions/apps';
|
||||
import {appsEnabled} from '@utils/apps';
|
||||
import {AppCallResponse} from '@mm-redux/types/apps';
|
||||
|
||||
export function executeCommand(message: string, channelId: string, rootId: string, intl: typeof intlShape): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
@@ -24,6 +26,7 @@ export function executeCommand(message: string, channelId: string, rootId: strin
|
||||
channel_id: channelId,
|
||||
team_id: teamId,
|
||||
root_id: rootId,
|
||||
parent_id: rootId,
|
||||
};
|
||||
|
||||
let msg = message;
|
||||
@@ -39,7 +42,7 @@ export function executeCommand(message: string, channelId: string, rootId: strin
|
||||
|
||||
const appsAreEnabled = appsEnabled(state);
|
||||
if (appsAreEnabled) {
|
||||
const parser = new AppCommandParser({dispatch, getState}, intl, args.channel_id, args.team_id, args.root_id);
|
||||
const parser = new AppCommandParser({dispatch, getState}, intl, args.channel_id, args.root_id);
|
||||
if (parser.isAppCommand(msg)) {
|
||||
const {call, errorMessage} = await parser.composeCallFromCommand(msg);
|
||||
const createErrorMessage = (errMessage: string) => {
|
||||
@@ -66,14 +69,8 @@ export function executeCommand(message: string, channelId: string, rootId: strin
|
||||
}
|
||||
return {data: {}};
|
||||
case AppCallResponseTypes.FORM:
|
||||
return {data: {
|
||||
form: callResp.form,
|
||||
call,
|
||||
}};
|
||||
case AppCallResponseTypes.NAVIGATE:
|
||||
return {data: {
|
||||
goto_location: callResp.navigate_to_url,
|
||||
}};
|
||||
return {data: {}};
|
||||
default:
|
||||
return createErrorMessage(intl.formatMessage({
|
||||
id: 'apps.error.responses.unknown_type',
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {addChannelToCategory} from '@mm-redux/actions/channel_categories';
|
||||
import {handleSelectChannel, setChannelDisplayName} from './channel';
|
||||
import {createChannel} from '@mm-redux/actions/channels';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {cleanUpUrlable} from '@mm-redux/utils/channel_utils';
|
||||
import {generateId} from '@mm-redux/utils/helpers';
|
||||
|
||||
import {handleSelectChannel, setChannelDisplayName} from './channel';
|
||||
|
||||
export function generateChannelNameFromDisplayName(displayName) {
|
||||
let name = cleanUpUrlable(displayName);
|
||||
|
||||
@@ -20,7 +18,7 @@ export function generateChannelNameFromDisplayName(displayName) {
|
||||
return name;
|
||||
}
|
||||
|
||||
export function handleCreateChannel(displayName, purpose, header, type, categoryId) {
|
||||
export function handleCreateChannel(displayName, purpose, header, type) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
@@ -38,10 +36,6 @@ export function handleCreateChannel(displayName, purpose, header, type, category
|
||||
if (data && data.id) {
|
||||
dispatch(setChannelDisplayName(displayName));
|
||||
dispatch(handleSelectChannel(data.id));
|
||||
|
||||
if (categoryId) {
|
||||
dispatch(addChannelToCategory(categoryId, data.id));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {generateChannelNameFromDisplayName} from '@actions/views/create_channel';
|
||||
import {generateChannelNameFromDisplayName} from 'app/actions/views/create_channel';
|
||||
|
||||
describe('Actions.Views.CreateChannel', () => {
|
||||
describe('generateChannelNameFromDisplayName', () => {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {PreferenceTypes} from '@mm-redux/action_types';
|
||||
import {General, Preferences} from '@mm-redux/constants';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getCollapsedThreadsPreference} from '@mm-redux/selectors/entities/preferences';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import type {ActionResult, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
|
||||
import type {PreferenceType} from '@mm-redux/types/preferences';
|
||||
|
||||
export function handleCRTPreferenceChange(preferences: PreferenceType[]) {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
|
||||
const state = getState();
|
||||
const newCRTPreference = preferences.find((preference) => preference.name === Preferences.COLLAPSED_REPLY_THREADS);
|
||||
if (newCRTPreference && getConfig(state).CollapsedThreads !== undefined) {
|
||||
const newCRTValue = newCRTPreference.value;
|
||||
const oldCRTValue = getCollapsedThreadsPreference(state);
|
||||
if (newCRTValue !== oldCRTValue) {
|
||||
dispatch({
|
||||
type: PreferenceTypes.RECEIVED_PREFERENCES,
|
||||
data: preferences,
|
||||
});
|
||||
EventEmitter.emit(General.CRT_PREFERENCE_CHANGED, newCRTValue);
|
||||
return {data: true};
|
||||
}
|
||||
}
|
||||
return {data: false};
|
||||
};
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {UserTypes} from '@mm-redux/action_types';
|
||||
import {logError} from '@mm-redux/actions/errors';
|
||||
import {UserTypes} from '@mm-redux/action_types';
|
||||
import {getCurrentUser} from '@mm-redux/selectors/entities/common';
|
||||
import {ActionFunc, DispatchFunc, batchActions, GetStateFunc} from '@mm-redux/types/actions';
|
||||
import {UserCustomStatus} from '@mm-redux/types/users';
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ViewTypes} from '@constants';
|
||||
import {updateMe, setDefaultProfileImage} from '@mm-redux/actions/users';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export function updateUser(user, success, error) {
|
||||
return async (dispatch, getState) => {
|
||||
const result = await updateMe(user)(dispatch, getState);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import emojiRegex from 'emoji-regex';
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {EmojiTypes} from '@mm-redux/action_types';
|
||||
import {addReaction as serviceAddReaction, getNeededCustomEmojis} from '@mm-redux/actions/posts';
|
||||
import {Client4} from '@client/rest';
|
||||
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from '@mm-redux/selectors/entities/posts';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {EmojiIndicesByAlias, EmojiIndicesByUnicode, Emojis} from '@utils/emojis';
|
||||
import emojiRegex from 'emoji-regex';
|
||||
|
||||
const getPostIdsForThread = makeGetPostIdsForThread();
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ViewTypes} from '@constants';
|
||||
import {FileTypes} from '@mm-redux/action_types';
|
||||
import {buildFileUploadData, generateId} from '@utils/file';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
import {buildFileUploadData, generateId} from 'app/utils/file';
|
||||
|
||||
export function initUploadFiles(files, rootId) {
|
||||
return (dispatch, getState) => {
|
||||
|
||||
@@ -3,25 +3,27 @@
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import {loadConfigAndLicense} from '@actions/views/root';
|
||||
import {Client4} from '@client/rest';
|
||||
import {setAppCredentials} from '@init/credentials';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import {GeneralTypes} from '@mm-redux/action_types';
|
||||
import {getDataRetentionPolicy} from '@mm-redux/actions/general';
|
||||
import {autoUpdateTimezone} from '@mm-redux/actions/timezone';
|
||||
import {GeneralTypes} from '@mm-redux/action_types';
|
||||
import {getSessions} from '@mm-redux/actions/users';
|
||||
import {autoUpdateTimezone} from '@mm-redux/actions/timezone';
|
||||
import {Client4} from '@client/rest';
|
||||
import {getConfig, getLicense} from '@mm-redux/selectors/entities/general';
|
||||
import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {setCSRFFromCookie} from '@utils/security';
|
||||
import {getDeviceTimezone} from '@utils/timezone';
|
||||
|
||||
import {setAppCredentials} from 'app/init/credentials';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import {getDeviceTimezone} from 'app/utils/timezone';
|
||||
import {setCSRFFromCookie} from 'app/utils/security';
|
||||
import {loadConfigAndLicense} from 'app/actions/views/root';
|
||||
|
||||
export function handleSuccessfulLogin() {
|
||||
return async (dispatch, getState) => {
|
||||
await dispatch(loadConfigAndLicense());
|
||||
|
||||
const state = getState();
|
||||
const config = getConfig(state);
|
||||
const license = getLicense(state);
|
||||
const token = Client4.getToken();
|
||||
const url = Client4.getUrl();
|
||||
@@ -44,7 +46,8 @@ export function handleSuccessfulLogin() {
|
||||
},
|
||||
});
|
||||
|
||||
if (license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
|
||||
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
|
||||
license.IsLicensed === 'true' && license.DataRetention === 'true') {
|
||||
dispatch(getDataRetentionPolicy());
|
||||
} else {
|
||||
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import {handleSuccessfulLogin} from '@actions/views/login';
|
||||
import {Client4} from '@client/rest';
|
||||
|
||||
import {handleSuccessfulLogin} from 'app/actions/views/login';
|
||||
|
||||
jest.mock('app/init/credentials', () => ({
|
||||
setAppCredentials: () => jest.fn(),
|
||||
}));
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {handleSelectChannel, toggleDMChannel, toggleGMChannel} from '@actions/views/channel';
|
||||
import {getDirectChannelName} from '@mm-redux/utils/channel_utils';
|
||||
import {createDirectChannel, createGroupChannel} from '@mm-redux/actions/channels';
|
||||
import {getProfilesByIds, getStatusesByIds} from '@mm-redux/actions/users';
|
||||
import {getDirectChannelName} from '@mm-redux/utils/channel_utils';
|
||||
import {handleSelectChannel, toggleDMChannel, toggleGMChannel} from 'app/actions/views/channel';
|
||||
|
||||
export function makeDirectChannel(otherUserId, switchToChannel = true) {
|
||||
return async (dispatch, getState) => {
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {UserTypes} from '@mm-redux/action_types';
|
||||
import {
|
||||
doPostAction,
|
||||
@@ -17,14 +15,16 @@ import {
|
||||
receivedPostsSince,
|
||||
receivedPostsInThread,
|
||||
} from '@mm-redux/actions/posts';
|
||||
import {Client4} from '@client/rest';
|
||||
import {Posts} from '@mm-redux/constants';
|
||||
import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
|
||||
import {getPost as selectPost, getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
||||
import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
|
||||
import {isUnreadChannel, isArchivedChannel} from '@mm-redux/utils/channel_utils';
|
||||
import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
|
||||
import {removeUserFromList} from '@mm-redux/utils/user_utils';
|
||||
import {getChannelSinceValue} from '@utils/channels';
|
||||
import {isUnreadChannel, isArchivedChannel} from '@mm-redux/utils/channel_utils';
|
||||
|
||||
import {ViewTypes} from '@constants';
|
||||
import {generateId} from '@utils/file';
|
||||
import {getChannelSinceValue} from '@utils/channels';
|
||||
|
||||
import {getEmojisInPosts} from './emoji';
|
||||
|
||||
@@ -40,6 +40,7 @@ export function sendEphemeralPost(message, channelId = '', parentId = '', userId
|
||||
create_at: timestamp,
|
||||
update_at: timestamp,
|
||||
root_id: parentId,
|
||||
parent_id: parentId,
|
||||
props: {},
|
||||
};
|
||||
|
||||
@@ -50,7 +51,7 @@ export function sendEphemeralPost(message, channelId = '', parentId = '', userId
|
||||
}
|
||||
|
||||
export function sendAddToChannelEphemeralPost(user, addedUsername, message, channelId, postRootId = '') {
|
||||
return async (dispatch, getState) => {
|
||||
return async (dispatch) => {
|
||||
const timestamp = Date.now();
|
||||
const post = {
|
||||
id: generateId(),
|
||||
@@ -61,14 +62,14 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, message, chan
|
||||
create_at: timestamp,
|
||||
update_at: timestamp,
|
||||
root_id: postRootId,
|
||||
parent_id: postRootId,
|
||||
props: {
|
||||
username: user.username,
|
||||
addedUsername,
|
||||
},
|
||||
};
|
||||
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
||||
dispatch(receivedNewPost(post, collapsedThreadsEnabled));
|
||||
dispatch(receivedNewPost(post));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -101,14 +102,13 @@ export function selectAttachmentMenuAction(postId, actionId, text, value) {
|
||||
};
|
||||
}
|
||||
|
||||
export function getPosts(channelId, page = 0, perPage = Posts.POST_CHUNK_SIZE, fetchThreads = true, collapsedThreadsExtended = false) {
|
||||
export function getPosts(channelId, page = 0, perPage = Posts.POST_CHUNK_SIZE) {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
const state = getState();
|
||||
const {postsInChannel} = state.entities.posts;
|
||||
const postForChannel = postsInChannel[channelId];
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
||||
const data = await Client4.getPosts(channelId, page, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
||||
const data = await Client4.getPosts(channelId, page, perPage);
|
||||
const posts = Object.values(data.posts);
|
||||
const actions = [{
|
||||
type: ViewTypes.SET_CHANNEL_RETRY_FAILED,
|
||||
@@ -161,11 +161,10 @@ export function getPost(postId) {
|
||||
};
|
||||
}
|
||||
|
||||
export function getPostsSince(channelId, since, fetchThreads = true, collapsedThreadsExtended = false) {
|
||||
return async (dispatch, getState) => {
|
||||
export function getPostsSince(channelId, since) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
||||
const data = await Client4.getPostsSince(channelId, since, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
||||
const data = await Client4.getPostsSince(channelId, since);
|
||||
const posts = Object.values(data.posts);
|
||||
|
||||
if (posts?.length) {
|
||||
@@ -189,11 +188,10 @@ export function getPostsSince(channelId, since, fetchThreads = true, collapsedTh
|
||||
};
|
||||
}
|
||||
|
||||
export function getPostsBefore(channelId, postId, page = 0, perPage = Posts.POST_CHUNK_SIZE, fetchThreads = true, collapsedThreadsExtended = false) {
|
||||
return async (dispatch, getState) => {
|
||||
export function getPostsBefore(channelId, postId, page = 0, perPage = Posts.POST_CHUNK_SIZE) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
||||
const data = await Client4.getPostsBefore(channelId, postId, page, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended);
|
||||
const data = await Client4.getPostsBefore(channelId, postId, page, perPage);
|
||||
const posts = Object.values(data.posts);
|
||||
|
||||
if (posts?.length) {
|
||||
@@ -248,14 +246,13 @@ export function getPostThread(rootId, skipDispatch = false) {
|
||||
};
|
||||
}
|
||||
|
||||
export function getPostsAround(channelId, postId, perPage = Posts.POST_CHUNK_SIZE / 2, fetchThreads = true, collapsedThreadsExtended = false) {
|
||||
return async (dispatch, getState) => {
|
||||
export function getPostsAround(channelId, postId, perPage = Posts.POST_CHUNK_SIZE / 2) {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(getState());
|
||||
const [before, thread, after] = await Promise.all([
|
||||
Client4.getPostsBefore(channelId, postId, 0, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended),
|
||||
Client4.getPostThread(postId, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended),
|
||||
Client4.getPostsAfter(channelId, postId, 0, perPage, fetchThreads, collapsedThreadsEnabled, collapsedThreadsExtended),
|
||||
Client4.getPostsBefore(channelId, postId, 0, perPage),
|
||||
Client4.getPostThread(postId),
|
||||
Client4.getPostsAfter(channelId, postId, 0, perPage),
|
||||
]);
|
||||
|
||||
const data = {
|
||||
@@ -300,8 +297,7 @@ export function handleNewPostBatch(WebSocketMessage) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const post = JSON.parse(WebSocketMessage.data.post);
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
|
||||
const actions = [receivedNewPost(post, collapsedThreadsEnabled)];
|
||||
const actions = [receivedNewPost(post)];
|
||||
|
||||
// If we don't have the thread for this post, fetch it from the server
|
||||
// and include the actions in the batch
|
||||
@@ -442,8 +438,8 @@ export function loadUnreadChannelPosts(channels, channelMembers) {
|
||||
if (channel.id === currentChannelId || isArchivedChannel(channel)) {
|
||||
return;
|
||||
}
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
|
||||
const isUnread = isUnreadChannel(channelMembersByChannel, channel, collapsedThreadsEnabled);
|
||||
|
||||
const isUnread = isUnreadChannel(channelMembersByChannel, channel);
|
||||
if (!isUnread) {
|
||||
return;
|
||||
}
|
||||
@@ -457,10 +453,10 @@ export function loadUnreadChannelPosts(channels, channelMembers) {
|
||||
};
|
||||
if (!postIds || !postIds.length) {
|
||||
// Get the first page of posts if it appears we haven't gotten it yet, like the webapp
|
||||
promise = Client4.getPosts(channel.id, undefined, undefined, true, collapsedThreadsEnabled);
|
||||
promise = Client4.getPosts(channel.id);
|
||||
} else {
|
||||
const since = getChannelSinceValue(state, channel.id, postIds);
|
||||
promise = Client4.getPostsSince(channel.id, since, true, collapsedThreadsEnabled);
|
||||
promise = Client4.getPostsSince(channel.id, since);
|
||||
trace.since = since;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,17 @@
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import {loadUnreadChannelPosts} from '@actions/views/post';
|
||||
import {Client4} from '@client/rest';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {PostTypes, UserTypes} from '@mm-redux/action_types';
|
||||
|
||||
import * as PostSelectors from '@mm-redux/selectors/entities/posts';
|
||||
import * as ChannelUtils from '@mm-redux/utils/channel_utils';
|
||||
|
||||
import {ViewTypes} from '@constants';
|
||||
import initialState from '@store/initial_state';
|
||||
|
||||
import {loadUnreadChannelPosts} from '@actions/views/post';
|
||||
|
||||
describe('Actions.Views.Post', () => {
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
||||
|
||||
@@ -3,22 +3,19 @@
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {NavigationTypes, ViewTypes} from '@constants';
|
||||
import {ChannelTypes, GeneralTypes, TeamTypes} from '@mm-redux/action_types';
|
||||
import {getChannelAndMyMember} from '@mm-redux/actions/channels';
|
||||
import {fetchMyChannelsAndMembers, getChannelAndMyMember} from '@mm-redux/actions/channels';
|
||||
import {getDataRetentionPolicy} from '@mm-redux/actions/general';
|
||||
import {receivedNewPost, selectPost} from '@mm-redux/actions/posts';
|
||||
import {getMyTeams, getMyTeamMembers, getMyTeamUnreads} from '@mm-redux/actions/teams';
|
||||
import {receivedNewPost} from '@mm-redux/actions/posts';
|
||||
import {getMyTeams, getMyTeamMembers} from '@mm-redux/actions/teams';
|
||||
import {Client4} from '@client/rest';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {getViewingGlobalThreads} from '@selectors/threads';
|
||||
import initialState from '@store/initial_state';
|
||||
import {getStateForReset} from '@store/utils';
|
||||
|
||||
import {loadChannelsForTeam, markAsViewedAndReadBatch} from './channel';
|
||||
import {handleNotViewingGlobalThreadsScreen} from './threads';
|
||||
import {markAsViewedAndReadBatch} from './channel';
|
||||
|
||||
export function startDataCleanup() {
|
||||
return async (dispatch, getState) => {
|
||||
@@ -48,7 +45,8 @@ export function loadConfigAndLicense() {
|
||||
}];
|
||||
|
||||
if (currentUserId) {
|
||||
if (license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
|
||||
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
|
||||
license.IsLicensed === 'true' && license.DataRetention === 'true') {
|
||||
dispatch(getDataRetentionPolicy());
|
||||
} else {
|
||||
actions.push({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
|
||||
@@ -64,11 +62,12 @@ export function loadConfigAndLicense() {
|
||||
};
|
||||
}
|
||||
|
||||
export function loadFromPushNotification(notification, isInitialNotification) {
|
||||
export function loadFromPushNotification(notification) {
|
||||
return async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const {payload} = notification;
|
||||
const {currentTeamId, teams, myMembers: myTeamMembers} = state.entities.teams;
|
||||
const {channels} = state.entities.channels;
|
||||
|
||||
let channelId = '';
|
||||
let teamId = currentTeamId;
|
||||
@@ -87,9 +86,8 @@ export function loadFromPushNotification(notification, isInitialNotification) {
|
||||
loading.push(dispatch(getMyTeamMembers()));
|
||||
}
|
||||
|
||||
if (isInitialNotification) {
|
||||
loading.push(dispatch(getMyTeamUnreads()));
|
||||
loading.push(dispatch(loadChannelsForTeam(teamId)));
|
||||
if (channelId && !channels[channelId]) {
|
||||
loading.push(dispatch(fetchMyChannelsAndMembers(teamId)));
|
||||
}
|
||||
|
||||
if (loading.length > 0) {
|
||||
@@ -97,12 +95,7 @@ export function loadFromPushNotification(notification, isInitialNotification) {
|
||||
}
|
||||
|
||||
dispatch(handleSelectTeamAndChannel(teamId, channelId));
|
||||
dispatch(selectPost(''));
|
||||
|
||||
const {root_id: rootId} = notification.payload || {};
|
||||
if (isCollapsedThreadsEnabled(state) && rootId) {
|
||||
dispatch(selectPost(rootId));
|
||||
}
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
@@ -110,27 +103,15 @@ export function loadFromPushNotification(notification, isInitialNotification) {
|
||||
export function handleSelectTeamAndChannel(teamId, channelId) {
|
||||
return async (dispatch, getState) => {
|
||||
const dt = Date.now();
|
||||
let state = getState();
|
||||
let {channels, myMembers} = state.entities.channels;
|
||||
await dispatch(getChannelAndMyMember(channelId));
|
||||
|
||||
if (channelId && (!channels[channelId] || !myMembers[channelId])) {
|
||||
await dispatch(getChannelAndMyMember(channelId));
|
||||
state = getState();
|
||||
}
|
||||
|
||||
channels = state.entities.channels.channels;
|
||||
myMembers = state.entities.channels.myMembers;
|
||||
|
||||
const {currentChannelId} = state.entities.channels;
|
||||
const state = getState();
|
||||
const {channels, currentChannelId, myMembers} = state.entities.channels;
|
||||
const {currentTeamId} = state.entities.teams;
|
||||
const channel = channels[channelId];
|
||||
const member = myMembers[channelId];
|
||||
const actions = markAsViewedAndReadBatch(state, channelId);
|
||||
|
||||
if (getViewingGlobalThreads(state)) {
|
||||
actions.push(handleNotViewingGlobalThreadsScreen());
|
||||
}
|
||||
|
||||
// when the notification is from a team other than the current team
|
||||
if (teamId !== currentTeamId) {
|
||||
actions.push({type: TeamTypes.SELECT_TEAM, data: teamId});
|
||||
@@ -167,7 +148,6 @@ export function purgeOfflineStore() {
|
||||
});
|
||||
|
||||
EventEmitter.emit(NavigationTypes.RESTART_APP);
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -190,8 +170,7 @@ export function createPostForNotificationReply(post) {
|
||||
|
||||
try {
|
||||
const data = await Client4.createPost({...newPost, create_at: 0});
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
|
||||
dispatch(receivedNewPost(data, collapsedThreadsEnabled));
|
||||
dispatch(receivedNewPost(data));
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ViewTypes} from '@constants';
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export function handleSearchDraftChanged(text) {
|
||||
return {
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {ViewTypes} from '@constants';
|
||||
import {GeneralTypes} from '@mm-redux/action_types';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
export function handleServerUrlChanged(serverUrl) {
|
||||
return batchActions([
|
||||
{type: GeneralTypes.CLIENT_CONFIG_RESET},
|
||||
|
||||
@@ -5,10 +5,12 @@ import {batchActions} from 'redux-batched-actions';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import {handleServerUrlChanged} from '@actions/views/select_server';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {GeneralTypes} from '@mm-redux/action_types';
|
||||
|
||||
import {ViewTypes} from 'app/constants';
|
||||
|
||||
import {handleServerUrlChanged} from 'app/actions/views/select_server';
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
||||
describe('Actions.Views.SelectServer', () => {
|
||||
|
||||
@@ -10,8 +10,8 @@ import {getMyTeams} from '@mm-redux/actions/teams';
|
||||
import {Preferences, RequestStatus} from '@mm-redux/constants';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {get as getPreference} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentLocale} from 'app/selectors/i18n';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {getCurrentLocale} from '@selectors/i18n';
|
||||
import {selectFirstAvailableTeam} from '@utils/teams';
|
||||
|
||||
export function handleTeamChange(teamId) {
|
||||
|
||||