Compare commits

..

20 Commits

Author SHA1 Message Date
Weblate (bot)
2cf7373157 Translations update from Weblate (#5628)
* Translated using Weblate (Hungarian)

Currently translated at 100.0% (754 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/

Translated using Weblate (Hungarian)

Currently translated at 100.0% (754 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (754 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/bg/

Co-authored-by: Tóth Csaba // Online ERP Hungary Kft <csaba.toth@online-erp.hu>
Co-authored-by: Nikolai Zahariev <nikolaiz@yahoo.com>
2021-08-16 11:07:16 +02:00
Mattermost Build
a5df05ee59 Bump app build number to 368 (#5625) (#5626)
(cherry picked from commit f7eecacccb)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-08-13 14:49:55 -04:00
Mattermost Build
c0fb9aa4ba [MM-37840, MM-37841] do not mark channel as read when clearing push notifications (#5623) (#5624)
* MM-37840 do not mark channel as read when clearing push notifications

* MM-37841 prevent from setting the edit post screen buttons with every key press

(cherry picked from commit a44a19d125)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-08-13 13:09:37 -04:00
Mattermost Build
4549ecdd94 Android Send Notification event to JS only once when the app is in the foreground (#5620) (#5621)
(cherry picked from commit 8953f29623)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-08-12 14:29:36 -04:00
Mattermost Build
b9179b0774 Bump app build number to 367 (#5616) (#5617)
(cherry picked from commit faa3ee05ba)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-08-11 10:02:55 -04:00
Mattermost Build
5d78720c2b MM-37485 MM-37602 MM-37610 MM-37684 MM-37719 MM-37522 MM-37720 App crash, Sync issue fixes (#5612) (#5615)
Summary
Fixes app crash on opening channel header
Fixes app crash on opening permalink from permalink screen
Fixes showing duplicates channels while searching in "Jump to..."
Fixes auto following the thread on opening the thread view when there are unreads
Fixes updation of threads state (Read/Unread/Deleted) of already loaded threads
Fixes counter showing as blank due to threads sync issue
Doesn't show Follow Thread/Message when CRT is not enabled

Ticket Link
https://mattermost.atlassian.net/browse/MM-37485
https://mattermost.atlassian.net/browse/MM-37602
https://mattermost.atlassian.net/browse/MM-37610
https://mattermost.atlassian.net/browse/MM-37684
https://mattermost.atlassian.net/browse/MM-37719
https://mattermost.atlassian.net/browse/MM-37522
https://mattermost.atlassian.net/browse/MM-37720

Device Information
This PR was tested on: iOS 14.5

(cherry picked from commit fc929891ea)

Co-authored-by: Anurag Shivarathri <anurag6713@gmail.com>
2021-08-11 16:49:22 +05:30
Weblate (bot)
ebeea7b27f Translations update from Weblate (#5614)
* Translated using Weblate (Hungarian)

Currently translated at 100.0% (754 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (754 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/sv/

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (754 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/

Translated using Weblate (Hungarian)

Currently translated at 99.8% (753 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/

Translated using Weblate (Hungarian)

Currently translated at 99.8% (753 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/

Translated using Weblate (Hungarian)

Currently translated at 99.8% (753 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/

* Translated using Weblate (Japanese)

Currently translated at 100.0% (754 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ja/

Co-authored-by: Zsolt Godó <zsolttokio@gmail.com>
Co-authored-by: MArtin Johnson <martinjohnson@bahnhof.se>
Co-authored-by: Tóth Csaba // Online ERP Hungary Kft <csaba.toth@online-erp.hu>
Co-authored-by: kaakaa <stooner.hoe@gmail.com>
2021-08-10 10:51:16 +02:00
Weblate (bot)
43aad265ac Translations update from Weblate (#5587)
* Translated using Weblate (Russian)

Currently translated at 100.0% (752 of 752 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ru/

Translated using Weblate (Russian)

Currently translated at 99.6% (749 of 752 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ru/

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (754 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/

Translated using Weblate (Hungarian)

Currently translated at 100.0% (752 of 752 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/

Translated using Weblate (Hungarian)

Currently translated at 99.3% (747 of 752 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/hu/

* Translated using Weblate (Japanese)

Currently translated at 95.2% (716 of 752 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/ja/

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (754 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/zh_Hans/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (752 of 752 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/zh_Hans/

* Translated using Weblate (German)

Currently translated at 100.0% (754 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/de/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (754 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/tr/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (754 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/es/

* Translated using Weblate (Dutch)

Currently translated at 99.4% (750 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/nl/

* Translated using Weblate (English (Australia))

Currently translated at 100.0% (754 of 754 strings)

Translation: mattermost-languages-shipped/mattermost-mobile
Translate-URL: https://translate.mattermost.com/projects/mattermost/mattermost-mobile_master/en_AU/

Co-authored-by: Alexey Napalkov <flynbit@gmail.com>
Co-authored-by: Tóth Csaba // Online ERP Hungary Kft <csaba.toth@online-erp.hu>
Co-authored-by: kaakaa <stooner.hoe@gmail.com>
Co-authored-by: aeomin <lin@aeomin.net>
Co-authored-by: JtheBAB <srast@bioc.uzh.ch>
Co-authored-by: Kaya Zeren <kayazeren@gmail.com>
Co-authored-by: Elias  Nahum <elias@mattermost.com>
Co-authored-by: Tom De Moor <tom@controlaltdieliet.be>
Co-authored-by: Matthew Williams <Matthew.Williams@outlook.com.au>
2021-08-10 10:28:28 +02:00
Mattermost Build
129d0a5189 MM-37608 and MM-37416 issue fixes (#5592) (#5613) 2021-08-09 23:05:59 -04:00
Mattermost Build
bb26ce5395 MM-37577 Do not set post id when posting a message (#5602) (#5608)
* MM-37577 Do not set post id when posting a message

* Add unit test to verify that creating a post always send an empty string as the id

(cherry picked from commit 7cdbe095a1)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-08-06 11:00:18 -04:00
Mattermost Build
c4b48dd647 Specify screenId for files uploads handler (#5595) (#5607)
(cherry picked from commit 4837ff5d70)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-08-06 09:49:57 -04:00
Mattermost Build
705d13c3ef MM-37674 Fix android push notification crash (#5601) (#5606)
(cherry picked from commit fdf93ae0c5)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-08-06 09:17:19 -04:00
Mattermost Build
0123c631be MM-37666 Properly dismiss edit profile screen (#5599) (#5605)
* MM-37666 Properly dismiss edit profile screen

* pass componentId to modal being dismissed

(cherry picked from commit 3ac73e808b)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-08-06 09:17:08 -04:00
Mattermost Build
71f80a033e MM-37660 Fix keep mention for channel A when opening a push notification on channel B (#5597) (#5604)
* Fix keep mention for channel A when opening a push notification on channel B

* properly fix race condition

* Load channels and channel members when opening the app from a push notification

(cherry picked from commit a7b604e8c9)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-08-06 08:58:53 -04:00
Mattermost Build
beaac8f373 Fix android share extension crash (#5596) (#5598) 2021-08-05 01:46:15 -04:00
Mattermost Build
485757996a Bump app build number to 366 (#5584) (#5585)
(cherry picked from commit 813af2199d)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-07-30 10:55:21 -04:00
Mattermost Build
ae94ef24a5 MM-37294 MM-37296 MM-37297 MM-37495 MM-37509 CRT fixes (#5574) (#5583) 2021-07-30 03:28:28 -04:00
Mattermost Build
43689751dc Upgrade Dependencies (#5578) (#5582)
* Dependency updates

* Ensure post list starts at the beginning on channel switch

* add missing prop to settings_sidebar test

* Update detox deps

(cherry picked from commit 29fd07ede6)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
2021-07-29 19:17:04 -04:00
Mattermost Build
2b1a30d757 Update NOTICE.txt (#5579) (#5580)
(cherry picked from commit a70ca4bac3)

Co-authored-by: Amy Blais <29708087+amyblais@users.noreply.github.com>
2021-07-29 17:21:53 -04:00
Mattermost Build
17296fec36 MM-37456 "Threads" item in the sidebar should not scroll (#5575) (#5577)
* Made threads item as static

* Updated snapshot

* Moved from Text to FormattedText

* Updated snapshot .. AGAIN

* Removed 8px margin bottom from threads sidebar item

(cherry picked from commit 2a641afc66)

Co-authored-by: Anurag Shivarathri <anurag6713@gmail.com>
2021-07-29 10:48:15 -04:00
844 changed files with 33386 additions and 28262 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -532,29 +532,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.

View File

@@ -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+

View File

@@ -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 368
versionName "1.46.0"
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'
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -0,0 +1,7 @@
package com.mattermost.rnbeta;
import android.net.Uri;
public interface RNEditTextOnPasteListener {
void onPaste(Uri itemUri);
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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
);
}
}

View File

@@ -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;
}
}

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 787 KiB

BIN
android/app/src/main/res/drawable-mdpi/splash.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

BIN
android/app/src/main/res/drawable-xhdpi/splash.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

BIN
android/app/src/main/res/drawable-xxhdpi/splash.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

BIN
android/app/src/main/res/drawable-xxxhdpi/splash.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 855 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -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>

View File

@@ -2,5 +2,4 @@
<resources>
<color name="white">#FFFFFF</color>
<color name="transparent">#00000000</color>
<color name="splashscreen_bg">#FFFFFF</color>
</resources>
</resources>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -3,15 +3,22 @@
import {sendEphemeralPost} from '@actions/views/post';
import {Client4} from '@client/rest';
import CompassIcon from '@components/compass_icon';
import {handleGotoLocation} from '@mm-redux/actions/integrations';
import {AppCallTypes, AppCallResponseTypes} from '@mm-redux/constants/apps';
import {getTheme} from '@mm-redux/selectors/entities/preferences';
import {ActionFunc, DispatchFunc} from '@mm-redux/types/actions';
import {AppCallResponse, AppCallRequest, AppCallType, AppContext} from '@mm-redux/types/apps';
import {AppCallResponse, AppForm, AppCallRequest, AppCallType, AppContext} from '@mm-redux/types/apps';
import {CommandArgs} from '@mm-redux/types/integrations';
import {Post} from '@mm-redux/types/posts';
import {cleanForm, makeCallErrorResponse} from '@utils/apps';
import {Theme} from '@mm-redux/types/preferences';
import EphemeralStore from '@store/ephemeral_store';
import {makeCallErrorResponse} from '@utils/apps';
import {showModal} from './navigation';
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 +37,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 +61,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 +84,34 @@ 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;
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 function postEphemeralCallResponseForPost(response: AppCallResponse, message: string, post: Post): ActionFunc {
return (dispatch: DispatchFunc) => {
return dispatch(sendEphemeralPost(

View File

@@ -328,8 +328,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 +355,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;

View File

@@ -5,7 +5,6 @@ import merge from 'deepmerge';
import {Keyboard, Platform} from 'react-native';
import {Navigation} from 'react-native-navigation';
import CompassIcon from '@components/compass_icon';
import {DeviceTypes, NavigationTypes} from '@constants';
import {CHANNEL} from '@constants/screen';
import {Preferences} from '@mm-redux/constants';
@@ -92,7 +91,7 @@ export function resetToChannel(passProps = {}) {
}
export function resetToSelectServer(allowOtherServers) {
const theme = Preferences.THEMES.denim;
const theme = Preferences.THEMES.default;
EphemeralStore.clearNavigationComponents();
@@ -292,7 +291,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) {
@@ -373,34 +372,6 @@ 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;
@@ -457,13 +428,6 @@ export function showOverlay(name, passProps, options = {}) {
overlay: {
interceptTouchOutside: false,
},
...Platform.select({
android: {
statusBar: {
drawBehind: true,
},
},
}),
};
Navigation.showOverlay({

View File

@@ -37,7 +37,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',
};

View File

@@ -1,8 +1,6 @@
// 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';
@@ -12,7 +10,6 @@ import {ViewTypes} from '@constants';
import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from '@constants/post_draft';
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,
@@ -36,11 +33,11 @@ import {getTeamByName as selectTeamByName, getCurrentTeam, getTeamMemberships} f
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
import {getChannelByName as selectChannelByName, getChannelsIdForTeam} from '@mm-redux/utils/channel_utils';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
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';
@@ -640,10 +637,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 +745,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,

View File

@@ -166,12 +166,6 @@ describe('Actions.Views.Channel', () => {
[currentTeamId]: {},
},
},
general: {
config: {
EnableLegacySidebar: 'true',
},
serverVersion: '5.12.0',
},
},
};
@@ -180,7 +174,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);

View File

@@ -24,6 +24,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 +40,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 +67,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',

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {addChannelToCategory} from '@mm-redux/actions/channel_categories';
import {createChannel} from '@mm-redux/actions/channels';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
@@ -20,7 +19,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 +37,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));
}
}
};
}

View File

@@ -40,6 +40,7 @@ export function sendEphemeralPost(message, channelId = '', parentId = '', userId
create_at: timestamp,
update_at: timestamp,
root_id: parentId,
parent_id: parentId,
props: {},
};
@@ -61,6 +62,7 @@ export function sendAddToChannelEphemeralPost(user, addedUsername, message, chan
create_at: timestamp,
update_at: timestamp,
root_id: postRootId,
parent_id: postRootId,
props: {
username: user.username,
addedUsername,

View File

@@ -8,7 +8,7 @@ import {NavigationTypes, ViewTypes} from '@constants';
import {ChannelTypes, GeneralTypes, TeamTypes} from '@mm-redux/action_types';
import {getChannelAndMyMember} from '@mm-redux/actions/channels';
import {getDataRetentionPolicy} from '@mm-redux/actions/general';
import {receivedNewPost, selectPost} from '@mm-redux/actions/posts';
import {receivedNewPost} from '@mm-redux/actions/posts';
import {getMyTeams, getMyTeamMembers, getMyTeamUnreads} from '@mm-redux/actions/teams';
import {General} from '@mm-redux/constants';
import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
@@ -97,12 +97,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};
};
}

View File

@@ -3,9 +3,11 @@
import moment from 'moment-timezone';
import {Client4} from '@client/rest';
import PushNotifications from '@init/push_notifications';
import {getSessions} from '@mm-redux/actions/users';
import {getConfig} from '@mm-redux/selectors/entities/general';
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
const sortByNewest = (a, b) => {
if (a.create_at > b.create_at) {
@@ -21,7 +23,7 @@ export function scheduleExpiredNotification(intl) {
const {currentUserId} = state.entities.users;
const config = getConfig(state);
if (config.ExtendSessionLengthWithActivity === 'true') {
if (isMinimumServerVersion(Client4.serverVersion, 5, 24) && config.ExtendSessionLengthWithActivity === 'true') {
PushNotifications.cancelAllLocalNotifications();
return;
}

View File

@@ -1,8 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {fetchAppBindings, fetchThreadAppBindings} from '@mm-redux/actions/apps';
import {getThreadAppsBindingsChannelId} from '@mm-redux/selectors/entities/apps';
import {fetchAppBindings} from '@mm-redux/actions/apps';
import {getCurrentChannelId} from '@mm-redux/selectors/entities/common';
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
import {ActionResult, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
@@ -11,17 +10,9 @@ import {appsEnabled} from '@utils/apps';
export function handleRefreshAppsBindings() {
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
const state = getState();
if (!appsEnabled(state)) {
return {data: true};
if (appsEnabled(state)) {
dispatch(fetchAppBindings(getCurrentUserId(state), getCurrentChannelId(state)));
}
dispatch(fetchAppBindings(getCurrentUserId(state), getCurrentChannelId(state)));
const threadChannelID = getThreadAppsBindingsChannelId(state);
if (threadChannelID) {
dispatch(fetchThreadAppBindings(getCurrentUserId(state), threadChannelID));
}
return {data: true};
};
}

View File

@@ -1,67 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {debounce} from 'underscore';
import {fetchMyCategories, receivedCategoryOrder} from '@mm-redux/actions/channel_categories';
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
import {ActionResult, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
import {WebSocketMessage} from '@mm-redux/types/websocket';
const fetchCats = debounce((dispatch: DispatchFunc, teamId: string) => dispatch(fetchMyCategories(teamId)), 1000);
export function handleSidebarCategoryCreated(msg: WebSocketMessage) {
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
const state = getState();
const currentTeamId = getCurrentTeamId(state);
if (msg.broadcast.team_id !== currentTeamId) {
// The new category will be loaded when we switch teams.
return {data: false};
}
// Fetch all categories, including ones that weren't explicitly updated, in case any other categories had channels
// moved out of them.
dispatch(fetchMyCategories(msg.broadcast.team_id));
return {data: true};
};
}
export function handleSidebarCategoryUpdated(msg: WebSocketMessage) {
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
const state = getState();
if (msg.broadcast.team_id !== getCurrentTeamId(state)) {
// The updated categories will be loaded when we switch teams.
return {data: false};
}
// Fetch all categories in case any other categories had channels moved out of them.
// dispatch(fetchMyCategories(msg.broadcast.team_id));
fetchCats(dispatch, msg.broadcast.team_id);
return {data: true};
};
}
export function handleSidebarCategoryDeleted(msg: WebSocketMessage) {
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
const state = getState();
if (msg.broadcast.team_id !== getCurrentTeamId(state)) {
// The category will be removed when we switch teams.
return {data: false};
}
// Fetch all categories since any channels that were in the deleted category were moved to other categories.
dispatch(fetchMyCategories(msg.broadcast.team_id));
return {data: true};
};
}
export function handleSidebarCategoryOrderUpdated(msg: WebSocketMessage) {
return receivedCategoryOrder(msg.broadcast.team_id, msg.data.order);
}

View File

@@ -36,7 +36,7 @@ describe('Websocket Chanel Events', () => {
});
afterAll(async () => {
Actions.close();
Actions.close()();
mockServer.stop();
await TestHelper.tearDown();
});

View File

@@ -6,7 +6,6 @@ import {loadChannelsForTeam} from '@actions/views/channel';
import {Client4} from '@client/rest';
import {WebsocketEvents} from '@constants';
import {ChannelTypes, TeamTypes, RoleTypes} from '@mm-redux/action_types';
import {addChannelToInitialCategory} from '@mm-redux/actions/channel_categories';
import {markChannelAsRead} from '@mm-redux/actions/channels';
import {General} from '@mm-redux/constants';
import {
@@ -23,7 +22,6 @@ import {ActionResult, DispatchFunc, GenericAction, GetStateFunc, batchActions} f
import {WebSocketMessage} from '@mm-redux/types/websocket';
import {getChannelByName} from '@mm-redux/utils/channel_utils';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {shouldShowLegacySidebar} from '@utils/categories';
export function handleChannelConvertedEvent(msg: WebSocketMessage) {
return (dispatch: DispatchFunc, getState: GetStateFunc): ActionResult => {
@@ -49,9 +47,12 @@ export function handleChannelCreatedEvent(msg: WebSocketMessage) {
const currentTeamId = getCurrentTeamId(state);
if (teamId === currentTeamId && !channels[channelId]) {
return dispatch(fetchChannelAndAddToSidebar(msg.broadcast.channel_id, 'BATCH_WS_CHANNEL_CREATED'));
const channelActions = await fetchChannelAndMyMember(msg.broadcast.channel_id);
if (channelActions.length) {
dispatch(batchActions(channelActions, 'BATCH_WS_CHANNEL_CREATED'));
}
}
return {data: false};
return {data: true};
};
}
@@ -117,7 +118,11 @@ export function handleChannelMemberUpdatedEvent(msg: WebSocketMessage) {
export function handleChannelSchemeUpdatedEvent(msg: WebSocketMessage) {
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
return dispatch(fetchChannelAndAddToSidebar(msg.broadcast.channel_id, 'BATCH_WS_SCHEME_UPDATE'));
const channelActions = await fetchChannelAndMyMember(msg.broadcast.channel_id);
if (channelActions.length) {
dispatch(batchActions(channelActions, 'BATCH_WS_SCHEME_UPDATE'));
}
return {data: true};
};
}
@@ -195,7 +200,11 @@ export function handleChannelViewedEvent(msg: WebSocketMessage) {
export function handleDirectAddedEvent(msg: WebSocketMessage) {
return async (dispatch: DispatchFunc): Promise<ActionResult> => {
return dispatch(fetchChannelAndAddToSidebar(msg.broadcast.channel_id, 'BATCH_WS_DM_ADDED'));
const channelActions = await fetchChannelAndMyMember(msg.broadcast.channel_id);
if (channelActions.length) {
dispatch(batchActions(channelActions, 'BATCH_WS_DM_ADDED'));
}
return {data: true};
};
}
@@ -227,23 +236,3 @@ export function handleUpdateMemberRoleEvent(msg: WebSocketMessage) {
};
}
export function fetchChannelAndAddToSidebar(channelId: string, type?: string) {
return async (dispatch: DispatchFunc, getState: GetStateFunc): Promise<ActionResult> => {
const channelActions = await fetchChannelAndMyMember(channelId);
let channel;
if (channelActions.length) {
channel = channelActions.find((el) => el.type === ChannelTypes.RECEIVED_CHANNEL);
dispatch(batchActions(channelActions, type));
}
const state = getState();
if (channel && !shouldShowLegacySidebar(state)) {
dispatch(addChannelToInitialCategory(channel.data));
return {data: true};
}
return {data: false};
};
}

View File

@@ -28,7 +28,7 @@ describe('Websocket General Events', () => {
});
afterAll(async () => {
Actions.close();
Actions.close()();
mockServer.stop();
await TestHelper.tearDown();
});

View File

@@ -22,12 +22,12 @@ import {GlobalState} from '@mm-redux/types/store';
import {TeamMembership} from '@mm-redux/types/teams';
import {WebSocketMessage} from '@mm-redux/types/websocket';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
import {removeUserFromList} from '@mm-redux/utils/user_utils';
import {getChannelSinceValue} from '@utils/channels';
import websocketClient from '@websocket';
import {handleRefreshAppsBindings} from './apps';
import {handleSidebarCategoryCreated, handleSidebarCategoryDeleted, handleSidebarCategoryOrderUpdated, handleSidebarCategoryUpdated} from './categories';
import {
handleChannelConvertedEvent,
handleChannelCreatedEvent,
@@ -99,7 +99,7 @@ export function doFirstConnect(now: number) {
const {lastDisconnectAt} = state.websocket;
const actions: Array<GenericAction> = [wsConnected(now)];
if (lastDisconnectAt) {
if (isMinimumServerVersion(Client4.getServerVersion(), 5, 14) && lastDisconnectAt) {
const currentUserId = getCurrentUserId(state);
const users = getUsers(state);
const userIds = Object.keys(users);
@@ -220,7 +220,7 @@ export function doReconnect(now: number) {
});
}
if (lastDisconnectAt) {
if (isMinimumServerVersion(Client4.getServerVersion(), 5, 14) && lastDisconnectAt) {
const userIds = Object.keys(users);
const userUpdates = await Client4.getProfilesByIds(userIds, {since: lastDisconnectAt});
@@ -409,16 +409,9 @@ function handleEvent(msg: WebSocketMessage) {
return dispatch(handleThreadReadChanged(msg));
case WebsocketEvents.THREAD_FOLLOW_CHANGED:
return dispatch(handleThreadFollowChanged(msg));
case WebsocketEvents.APPS_FRAMEWORK_REFRESH_BINDINGS:
case WebsocketEvents.APPS_FRAMEWORK_REFRESH_BINDINGS: {
return dispatch(handleRefreshAppsBindings());
case WebsocketEvents.SIDEBAR_CATEGORY_CREATED:
return dispatch(handleSidebarCategoryCreated(msg));
case WebsocketEvents.SIDEBAR_CATEGORY_UPDATED:
return dispatch(handleSidebarCategoryUpdated(msg));
case WebsocketEvents.SIDEBAR_CATEGORY_DELETED:
return dispatch(handleSidebarCategoryDeleted(msg));
case WebsocketEvents.SIDEBAR_CATEGORY_ORDER_UPDATED:
return dispatch(handleSidebarCategoryOrderUpdated(msg));
}
}
return {data: true};

View File

@@ -26,7 +26,7 @@ describe('Websocket Integration Events', () => {
});
afterAll(async () => {
Actions.close();
Actions.close()();
mockServer.stop();
await TestHelper.tearDown();
});

View File

@@ -34,7 +34,7 @@ describe('Websocket Post Events', () => {
});
afterAll(async () => {
Actions.close();
Actions.close()();
mockServer.stop();
await TestHelper.tearDown();
});
@@ -42,7 +42,7 @@ describe('Websocket Post Events', () => {
it('Websocket Handle New Post if post does not exist', async () => {
PostSelectors.getPost = jest.fn();
const channelId = TestHelper.basicChannel.id;
const message = JSON.stringify({event: WebsocketEvents.POSTED, data: {channel_display_name: TestHelper.basicChannel.display_name, channel_name: TestHelper.basicChannel.name, channel_type: 'O', post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w", "create_at": 1508245311774, "update_at": 1508245311774, "edit_at": 0, "delete_at": 0, "is_pinned": false, "user_id": "${TestHelper.basicUser.id}", "channel_id": "${channelId}", "root_id": "", "original_id": "", "message": "Unit Test", "type": "", "props": {}, "hashtags": "", "pending_post_id": "t36kso9nwtdhbm8dbkd6g4eeby: 1508245311749"}`, sender_name: TestHelper.basicUser.username, team_id: TestHelper.basicTeam.id}, broadcast: {omit_users: null, user_id: '', channel_id: channelId, team_id: ''}, seq: 2});
const message = JSON.stringify({event: WebsocketEvents.POSTED, data: {channel_display_name: TestHelper.basicChannel.display_name, channel_name: TestHelper.basicChannel.name, channel_type: 'O', post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w", "create_at": 1508245311774, "update_at": 1508245311774, "edit_at": 0, "delete_at": 0, "is_pinned": false, "user_id": "${TestHelper.basicUser.id}", "channel_id": "${channelId}", "root_id": "", "parent_id": "", "original_id": "", "message": "Unit Test", "type": "", "props": {}, "hashtags": "", "pending_post_id": "t36kso9nwtdhbm8dbkd6g4eeby: 1508245311749"}`, sender_name: TestHelper.basicUser.username, team_id: TestHelper.basicTeam.id}, broadcast: {omit_users: null, user_id: '', channel_id: channelId, team_id: ''}, seq: 2});
nock(Client4.getBaseRoute()).
post('/users/ids').
@@ -62,7 +62,7 @@ describe('Websocket Post Events', () => {
// Mock that post does not exist and check it is added
PostSelectors.getPost.mockReturnValueOnce(false);
mockServer.emit('message', message);
await TestHelper.wait(300);
await TestHelper.wait(100);
entities = store.getState().entities;
posts = entities.posts.posts;
const postId = Object.keys(posts)[0];
@@ -75,7 +75,7 @@ describe('Websocket Post Events', () => {
const emit = jest.spyOn(EventEmitter, 'emit');
const currentChannelId = TestHelper.generateId();
const otherChannelId = TestHelper.generateId();
const messageFor = (channelId) => ({event: WebsocketEvents.POSTED, data: {channel_display_name: TestHelper.basicChannel.display_name, channel_name: TestHelper.basicChannel.name, channel_type: 'O', post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w", "create_at": 1508245311774, "update_at": 1508245311774, "edit_at": 0, "delete_at": 0, "is_pinned": false, "user_id": "${TestHelper.basicUser.id}", "channel_id": "${channelId}", "root_id": "", "original_id": "", "message": "Unit Test", "type": "", "props": {}, "hashtags": "", "pending_post_id": "t36kso9nwtdhbm8dbkd6g4eeby: 1508245311749"}`, sender_name: TestHelper.basicUser.username, team_id: TestHelper.basicTeam.id}, broadcast: {omit_users: null, user_id: '', channel_id: channelId, team_id: ''}, seq: 2});
const messageFor = (channelId) => ({event: WebsocketEvents.POSTED, data: {channel_display_name: TestHelper.basicChannel.display_name, channel_name: TestHelper.basicChannel.name, channel_type: 'O', post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w", "create_at": 1508245311774, "update_at": 1508245311774, "edit_at": 0, "delete_at": 0, "is_pinned": false, "user_id": "${TestHelper.basicUser.id}", "channel_id": "${channelId}", "root_id": "", "parent_id": "", "original_id": "", "message": "Unit Test", "type": "", "props": {}, "hashtags": "", "pending_post_id": "t36kso9nwtdhbm8dbkd6g4eeby: 1508245311749"}`, sender_name: TestHelper.basicUser.username, team_id: TestHelper.basicTeam.id}, broadcast: {omit_users: null, user_id: '', channel_id: channelId, team_id: ''}, seq: 2});
await store.dispatch(ChannelActions.selectChannel(currentChannelId));
await TestHelper.wait(100);
@@ -125,7 +125,7 @@ describe('Websocket Post Events', () => {
channel_display_name: TestHelper.basicChannel.display_name,
channel_name: TestHelper.basicChannel.name,
channel_type: 'O',
post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w", "create_at": 1508245311774, "update_at": 1508245311774, "edit_at": 0, "delete_at": 0, "is_pinned": false, "user_id": "${userId}", "channel_id": "${channelId}", "root_id": "", "original_id": "", "message": "Unit Test", "type": "", "props": {}, "hashtags": "", "pending_post_id": "t36kso9nwtdhbm8dbkd6g4eeby: 1508245311749"}`,
post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w", "create_at": 1508245311774, "update_at": 1508245311774, "edit_at": 0, "delete_at": 0, "is_pinned": false, "user_id": "${userId}", "channel_id": "${channelId}", "root_id": "", "parent_id": "", "original_id": "", "message": "Unit Test", "type": "", "props": {}, "hashtags": "", "pending_post_id": "t36kso9nwtdhbm8dbkd6g4eeby: 1508245311749"}`,
sender_name: TestHelper.basicUser.username,
team_id: TestHelper.basicTeam.id,
},
@@ -146,7 +146,7 @@ describe('Websocket Post Events', () => {
it('Websocket Handle Post Edited', async () => {
const post = {id: '71k8gz5ompbpfkrzaxzodffj8w'};
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.POST_EDITED, data: {post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w","create_at": 1508245311774,"update_at": 1585236976007,"edit_at": 1585236976007,"delete_at": 0,"is_pinned": false,"user_id": "${TestHelper.basicUser.id}","channel_id": "${TestHelper.basicChannel.id}","root_id": "","original_id": "","message": "Unit Test (edited)","type": "","props": {},"hashtags": "","pending_post_id": ""}`}, broadcast: {omit_users: null, user_id: '', channel_id: '18k9ffsuci8xxm7ak68zfdyrce', team_id: ''}, seq: 2}));
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.POST_EDITED, data: {post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w","create_at": 1508245311774,"update_at": 1585236976007,"edit_at": 1585236976007,"delete_at": 0,"is_pinned": false,"user_id": "${TestHelper.basicUser.id}","channel_id": "${TestHelper.basicChannel.id}","root_id": "","parent_id": "","original_id": "","message": "Unit Test (edited)","type": "","props": {},"hashtags": "","pending_post_id": ""}`}, broadcast: {omit_users: null, user_id: '', channel_id: '18k9ffsuci8xxm7ak68zfdyrce', team_id: ''}, seq: 2}));
await TestHelper.wait(300);
@@ -162,7 +162,7 @@ describe('Websocket Post Events', () => {
post.id = '71k8gz5ompbpfkrzaxzodffj8w';
store.dispatch(PostActions.receivedPost(post));
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.POST_DELETED, data: {post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w","create_at": 1508245311774,"update_at": 1508247709215,"edit_at": 1508247709215,"delete_at": 0,"is_pinned": false,"user_id": "${TestHelper.basicUser.id}","channel_id": "${post.channel_id}","root_id": "","original_id": "","message": "Unit Test","type": "","props": {},"hashtags": "","pending_post_id": ""}`}, broadcast: {omit_users: null, user_id: '', channel_id: '18k9ffsuci8xxm7ak68zfdyrce', team_id: ''}, seq: 7}));
mockServer.emit('message', JSON.stringify({event: WebsocketEvents.POST_DELETED, data: {post: `{"id": "71k8gz5ompbpfkrzaxzodffj8w","create_at": 1508245311774,"update_at": 1508247709215,"edit_at": 1508247709215,"delete_at": 0,"is_pinned": false,"user_id": "${TestHelper.basicUser.id}","channel_id": "${post.channel_id}","root_id": "","parent_id": "","original_id": "","message": "Unit Test","type": "","props": {},"hashtags": "","pending_post_id": ""}`}, broadcast: {omit_users: null, user_id: '', channel_id: '18k9ffsuci8xxm7ak68zfdyrce', team_id: ''}, seq: 7}));
const entities = store.getState().entities;
const {posts} = entities.posts;

View File

@@ -26,7 +26,7 @@ describe('Websocket Reaction Events', () => {
});
afterAll(async () => {
Actions.close();
Actions.close()();
mockServer.stop();
await TestHelper.tearDown();
});

View File

@@ -35,7 +35,7 @@ describe('Websocket Team Events', () => {
});
afterAll(async () => {
Actions.close();
Actions.close()();
mockServer.stop();
await TestHelper.tearDown();
});

View File

@@ -33,7 +33,7 @@ describe('Websocket User Events', () => {
});
afterAll(async () => {
Actions.close();
Actions.close()();
mockServer.stop();
await TestHelper.tearDown();
});

View File

@@ -64,7 +64,7 @@ describe('Actions.Websocket', () => {
});
afterAll(async () => {
Actions.close();
Actions.close()();
mockServer.stop();
await TestHelper.tearDown();
});
@@ -166,7 +166,7 @@ describe('Actions.Websocket doReconnect', () => {
});
afterAll(async () => {
Actions.close();
Actions.close()();
await TestHelper.tearDown();
});
@@ -373,7 +373,7 @@ describe('Actions.Websocket notVisibleUsersActions', () => {
const user4 = TestHelper.fakeUserWithId();
const user5 = TestHelper.fakeUserWithId();
it.skip('should do nothing if the known users and the profiles list are the same', async () => {
it('should do nothing if the known users and the profiles list are the same', async () => {
const profiles = {
[me.id]: me,
[user.id]: user,
@@ -397,7 +397,7 @@ describe('Actions.Websocket notVisibleUsersActions', () => {
expect(actions.length).toEqual(0);
});
it.skip('should do nothing if there are known users in my memberships but not in the profiles list', async () => {
it('should do nothing if there are known users in my memberships but not in the profiles list', async () => {
const profiles = {
[me.id]: me,
[user3.id]: user3,
@@ -419,7 +419,7 @@ describe('Actions.Websocket notVisibleUsersActions', () => {
expect(actions.length).toEqual(0);
});
it.skip('should remove the users if there are unknown users in the profiles list', async () => {
it('should remove the users if there are unknown users in the profiles list', async () => {
const profiles = {
[me.id]: me,
[user.id]: user,
@@ -450,6 +450,32 @@ describe('Actions.Websocket notVisibleUsersActions', () => {
expect(actions.length).toEqual(3);
expect(actions).toEqual(expectedAction);
});
it('should do nothing if the server version is less than 5.23', async () => {
const profiles = {
[me.id]: me,
[user.id]: user,
[user2.id]: user2,
[user3.id]: user3,
[user4.id]: user4,
[user5.id]: user5,
};
Client4.serverVersion = '5.22.0';
const state = {
entities: {
users: {
currentUserId: me.id,
profiles,
},
},
};
mockGetKnownUsersRequest([user.id, user3.id]);
const actions = await notVisibleUsersActions(state);
expect(actions.length).toEqual(0);
});
});
describe('Actions.Websocket handleUserTypingEvent', () => {
@@ -502,7 +528,7 @@ describe('Actions.Websocket handleUserTypingEvent', () => {
const expectedActionsTypes = [
WebsocketEvents.TYPING,
'BATCHING_REDUCER.BATCH',
UserTypes.RECEIVED_STATUSES,
];
await testStore.dispatch(Actions.handleUserTypingEvent(msg));

View File

@@ -2,7 +2,6 @@
// See LICENSE.txt for license information.
import {analytics} from '@init/analytics';
import {ChannelCategory, OrderedChannelCategories} from '@mm-redux/types/channel_categories';
import {Channel, ChannelMemberCountByGroup, ChannelMembership, ChannelNotifyProps, ChannelStats} from '@mm-redux/types/channels';
import {buildQueryString} from '@mm-redux/utils/helpers';
@@ -41,12 +40,6 @@ export interface ClientChannelsMix {
autocompleteChannelsForSearch: (teamId: string, name: string) => Promise<Channel[]>;
searchChannels: (teamId: string, term: string) => Promise<Channel[]>;
searchArchivedChannels: (teamId: string, term: string) => Promise<Channel[]>;
// Categories
getChannelCategories: (userId: string, teamId: string) => Promise<OrderedChannelCategories>;
getChannelCategory: () => Promise<ChannelCategory>;
updateChannelCategory: (userId: string, teamId: string, category: ChannelCategory) => Promise<OrderedChannelCategories>;
updateChannelCategories: (userId: string, teamId: string, categories: ChannelCategory[]) => Promise<OrderedChannelCategories>;
}
const ClientChannels = (superclass: any) => class extends superclass {
@@ -313,39 +306,6 @@ const ClientChannels = (superclass: any) => class extends superclass {
{method: 'post', body: JSON.stringify({term})},
);
};
// Channel Category Routes
getChannelCategoriesRoute(userId: string, teamId: string) {
return `${this.getUserRoute('me')}/teams/${teamId}/channels/categories`;
}
getChannelCategories = async (userId: string, teamId: string) => {
return this.doFetch(
`${this.getChannelCategoriesRoute(userId, teamId)}`,
{method: 'get'},
);
};
getChannelCategory = async (userId: string, teamId: string, categoryId: string) => {
return this.doFetch(
`${this.getChannelCategoriesRoute(userId, teamId)}/${categoryId}`,
{method: 'get'},
);
};
updateChannelCategory = (userId: string, teamId: string, category: ChannelCategory) => {
return this.doFetch(
`${this.getChannelCategoriesRoute(userId, teamId)}/${category.id}`,
{method: 'put', body: JSON.stringify(category)},
);
};
updateChannelCategories = (userId: string, teamId: string, categories: ChannelCategory[]) => {
return this.doFetch(
`${this.getChannelCategoriesRoute(userId, teamId)}`,
{method: 'put', body: JSON.stringify(categories)},
);
};
};
export default ClientChannels;

View File

@@ -4,7 +4,7 @@
import {analytics} from '@init/analytics';
import {FileInfo} from '@mm-redux/types/files';
import {Post} from '@mm-redux/types/posts';
import {buildQueryString, isMinimumServerVersion} from '@mm-redux/utils/helpers';
import {buildQueryString} from '@mm-redux/utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
@@ -126,16 +126,9 @@ const ClientPosts = (superclass: any) => class extends superclass {
);
};
getUserThreads = async (userId: string, teamId: string, before = '', after = '', perPage = PER_PAGE_DEFAULT, extended = false, deleted = false, unread = false, since = 0) => {
const serverVersion = this.getServerVersion();
let queryStringObj;
if (isMinimumServerVersion(serverVersion, 6, 0)) {
queryStringObj = {before, after, per_page: perPage, extended, deleted, unread, since};
} else {
queryStringObj = {before, after, pageSize: perPage, extended, deleted, unread, since};
}
getUserThreads = async (userId: string, teamId: string, before = '', after = '', pageSize = PER_PAGE_DEFAULT, extended = false, deleted = false, unread = false, since = 0) => {
return this.doFetch(
`${this.getUserThreadsRoute(userId, teamId)}${buildQueryString(queryStringObj)}`,
`${this.getUserThreadsRoute(userId, teamId)}${buildQueryString({before, after, pageSize, extended, deleted, unread, since})}`,
{method: 'get'},
);
};
@@ -250,13 +243,8 @@ const ClientPosts = (superclass: any) => class extends superclass {
searchPostsWithParams = async (teamId: string, params: any) => {
analytics.trackAPI('api_posts_search', {team_id: teamId});
let route = `${this.getPostsRoute()}/search`;
if (teamId) {
route = `${this.getTeamRoute(teamId)}/posts/search`;
}
return this.doFetch(
route,
`${this.getTeamRoute(teamId)}/posts/search`,
{method: 'post', body: JSON.stringify(params)},
);
};

View File

@@ -4,7 +4,7 @@
import {analytics} from '@init/analytics';
import {General} from '@mm-redux/constants';
import {UserCustomStatus, UserProfile, UserStatus} from '@mm-redux/types/users';
import {buildQueryString} from '@mm-redux/utils/helpers';
import {buildQueryString, isMinimumServerVersion} from '@mm-redux/utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
@@ -104,6 +104,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
getKnownUsers = async () => {
analytics.trackAPI('api_get_known_users');
return this.doFetch(
`${this.getUsersRoute()}/known`,
{method: 'get'},
@@ -254,7 +255,13 @@ const ClientUsers = (superclass: any) => class extends superclass {
getProfilesInChannel = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT, sort = '') => {
analytics.trackAPI('api_profiles_get_in_channel', {channel_id: channelId});
const queryStringObj = {in_channel: channelId, page, per_page: perPage, sort};
const serverVersion = this.getServerVersion();
let queryStringObj;
if (isMinimumServerVersion(serverVersion, 4, 7)) {
queryStringObj = {in_channel: channelId, page, per_page: perPage, sort};
} else {
queryStringObj = {in_channel: channelId, page, per_page: perPage};
}
return this.doFetch(
`${this.getUsersRoute()}${buildQueryString(queryStringObj)}`,
{method: 'get'},

View File

@@ -17,31 +17,31 @@ exports[`profile_picture_button should match snapshot 1`] = `
maxFileSize={20971520}
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#14213e",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
"type": "Mattermost",
}
}
uploadFiles={[MockFunction]}

View File

@@ -36,31 +36,31 @@ exports[`AnnouncementBanner should match snapshot 1`] = `
}
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#14213e",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
"type": "Mattermost",
}
}
/>
@@ -102,31 +102,31 @@ exports[`AnnouncementBanner should match snapshot 2`] = `
}
theme={
Object {
"awayIndicator": "#ffbc1f",
"buttonBg": "#1c58d9",
"awayIndicator": "#ffbc42",
"buttonBg": "#166de0",
"buttonColor": "#ffffff",
"centerChannelBg": "#ffffff",
"centerChannelColor": "#3f4350",
"centerChannelColor": "#3d3c40",
"codeTheme": "github",
"dndIndicator": "#d24b4e",
"errorTextColor": "#d24b4e",
"linkColor": "#386fe5",
"dndIndicator": "#f74343",
"errorTextColor": "#fd5960",
"linkColor": "#2389d7",
"mentionBg": "#ffffff",
"mentionColor": "#1e325c",
"mentionHighlightBg": "#ffd470",
"mentionHighlightLink": "#1b1d22",
"newMessageSeparator": "#cc8f00",
"onlineIndicator": "#3db887",
"sidebarBg": "#1e325c",
"sidebarHeaderBg": "#192a4d",
"mentionBj": "#ffffff",
"mentionColor": "#145dbf",
"mentionHighlightBg": "#ffe577",
"mentionHighlightLink": "#166de0",
"newMessageSeparator": "#ff8800",
"onlineIndicator": "#06d6a0",
"sidebarBg": "#145dbf",
"sidebarHeaderBg": "#1153ab",
"sidebarHeaderTextColor": "#ffffff",
"sidebarTeamBarBg": "#14213e",
"sidebarText": "#ffffff",
"sidebarTextActiveBorder": "#5d89ea",
"sidebarTextActiveBorder": "#579eff",
"sidebarTextActiveColor": "#ffffff",
"sidebarTextHoverBg": "#28427b",
"sidebarTextHoverBg": "#4578bf",
"sidebarUnreadText": "#ffffff",
"type": "Denim",
"type": "Mattermost",
}
}
/>

View File

@@ -17,7 +17,7 @@ describe('AnnouncementBanner', () => {
bannerEnabled: true,
bannerText: 'Banner Text',
bannerTextColor: '#fff',
theme: Preferences.THEMES.denim,
theme: Preferences.THEMES.default,
};
test('should match snapshot', () => {

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