forked from Ivasoft/mattermost-mobile
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86508004cf | ||
|
|
4f5bcdcb59 | ||
|
|
e25e179e42 | ||
|
|
62e1fdac7d |
@@ -57,7 +57,7 @@
|
||||
"newlines-between": "always",
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "@(@react-native-async-storage|@react-native-community|@react-native-cookies|@react-navigation|@rudderstack|@sentry|@testing-library|@storybook)/**",
|
||||
"pattern": "@(@react-native-community|@react-native-cookies|@react-navigation|@rudderstack|@sentry|@testing-library|@storybook)/**",
|
||||
"group": "external",
|
||||
"position": "before"
|
||||
},
|
||||
|
||||
@@ -25,8 +25,6 @@ emoji=true
|
||||
|
||||
exact_by_default=true
|
||||
|
||||
format.bracket_spacing=false
|
||||
|
||||
module.file_ext=.js
|
||||
module.file_ext=.json
|
||||
module.file_ext=.ios.js
|
||||
@@ -63,4 +61,4 @@ untyped-import
|
||||
untyped-type-import
|
||||
|
||||
[version]
|
||||
^0.158.0
|
||||
^0.149.0
|
||||
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -26,7 +26,6 @@ Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fie
|
||||
- [ ] Added or updated unit tests (required for all new features)
|
||||
- [ ] Has UI changes
|
||||
- [ ] Includes text changes and localization file updates
|
||||
- [ ] Have tested against the 5 core themes to ensure consistency between them.
|
||||
|
||||
#### Device Information
|
||||
This PR was tested on: <!-- Device name(s), OS version(s) -->
|
||||
|
||||
35
NOTICE.txt
35
NOTICE.txt
@@ -43,41 +43,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## @mattermost/react-native-paste-input
|
||||
|
||||
This product contains '@mattermost/react-native-paste-input' by Mattermost.
|
||||
|
||||
React Native TextInput component have functionality to capture text input from a user by using the soft and hardware keyboards but lacks the ability to restrict copy & paste options as well as allwing pasting different files formats copied from other apps, like images & videos from the Photos gallery app.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/mattermost/react-native-paste-input
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Elias Nahum
|
||||
|
||||
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/async-storage
|
||||
|
||||
This product contains 'async-storage' by Krzysztof Borowy.
|
||||
|
||||
10
README.md
10
README.md
@@ -1,14 +1,14 @@
|
||||
# Mattermost Mobile App
|
||||
[](https://mattermost.com)
|
||||
# Mattermost Mobile
|
||||
|
||||
- **Minimum Server versions:** Current ESR version (5.37.0)
|
||||
- **Supported iOS versions:** 12.1+
|
||||
- **Supported iOS versions:** 11+
|
||||
- **Supported Android versions:** 7.0+
|
||||
|
||||
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 14 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
|
||||
|
||||
[Mattermost](https://mattermost.com) is an open source platform for secure collaboration across the entire software development lifecycle. This repo is for the mobile app that runs on Android and iOS. You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or [build them yourself](https://developers.mattermost.com/contribute/mobile/build-your-own/).
|
||||
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or [build them yourself](https://developers.mattermost.com/contribute/mobile/build-your-own/).
|
||||
|
||||
New features are released monthly - check the [changelog](https://github.com/mattermost/mattermost-mobile/blob/master/CHANGELOG.md) for currently-supported features!
|
||||
We plan on releasing monthly updates with new features - check the [changelog](https://github.com/mattermost/mattermost-mobile/blob/master/CHANGELOG.md) for what features are currently supported!
|
||||
|
||||
**Important:** If you self-compile the Mattermost Mobile apps you also need to deploy your own [Mattermost Push Notification Service](https://github.com/mattermost/mattermost-push-proxy/releases).
|
||||
|
||||
|
||||
@@ -119,11 +119,6 @@ def jscFlavor = 'org.webkit:android-jsc-intl:+'
|
||||
*/
|
||||
def enableHermes = project.ext.react.get("enableHermes", false);
|
||||
|
||||
/**
|
||||
* Architectures to build native code for in debug.
|
||||
*/
|
||||
def nativeArchitectures = project.getProperties().get("reactNativeDebugArchitectures")
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
@@ -132,8 +127,8 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
||||
versionCode 382
|
||||
versionName "1.48.2"
|
||||
versionCode 370
|
||||
versionName "1.47.0"
|
||||
multiDexEnabled = true
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
@@ -165,11 +160,6 @@ android {
|
||||
debug {
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
if (nativeArchitectures) {
|
||||
ndk {
|
||||
abiFilters nativeArchitectures.split(',')
|
||||
}
|
||||
}
|
||||
}
|
||||
unsigned.initWith(buildTypes.release)
|
||||
unsigned {
|
||||
@@ -235,7 +225,7 @@ dependencies {
|
||||
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.fbjni'
|
||||
exclude group:'com.facebook.fbjni'
|
||||
}
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.flipper'
|
||||
|
||||
@@ -9,10 +9,6 @@
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
@@ -82,9 +78,5 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="com.google.android.youtube.api.service.START" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
||||
Binary file not shown.
@@ -15,6 +15,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
@@ -29,6 +30,7 @@ import static com.wix.reactnativenotifications.Defs.NOTIFICATION_RECEIVED_EVENT_
|
||||
|
||||
import com.mattermost.react_native_interface.ResolvePromise;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class CustomPushNotification extends PushNotification {
|
||||
@@ -59,7 +61,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
Map<String, Map<String, JSONObject>> inputMap = new HashMap<>();
|
||||
Map<String, List<Integer>> inputMap = new HashMap<>();
|
||||
saveNotificationsMap(context, inputMap);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
@@ -67,70 +69,55 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
}
|
||||
|
||||
public static void cancelNotification(Context context, String channelId, String rootId, Integer notificationId, Boolean isCRTEnabled) {
|
||||
public static void cancelNotification(Context context, String channelId, Integer notificationId) {
|
||||
if (!android.text.TextUtils.isEmpty(channelId)) {
|
||||
final String notificationIdStr = notificationId.toString();
|
||||
final Boolean isThreadNotification = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
final String groupId = isThreadNotification ? rootId : channelId;
|
||||
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
|
||||
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
|
||||
List<Integer> notifications = notificationsInChannel.get(channelId);
|
||||
if (notifications == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
|
||||
notificationManager.cancel(notificationId);
|
||||
notifications.remove(notificationIdStr);
|
||||
notifications.remove(notificationId);
|
||||
final StatusBarNotification[] statusNotifications = notificationManager.getActiveNotifications();
|
||||
boolean hasMore = false;
|
||||
for (final StatusBarNotification status : statusNotifications) {
|
||||
Bundle bundle = status.getNotification().extras;
|
||||
if (isThreadNotification) {
|
||||
hasMore = bundle.getString("root_id").equals(rootId);
|
||||
} else if (isCRTEnabled) {
|
||||
hasMore = !bundle.getString("root_id").equals(rootId);
|
||||
} else {
|
||||
hasMore = bundle.getString("channel_id").equals(channelId);
|
||||
}
|
||||
if (hasMore) {
|
||||
if (status.getNotification().extras.getString("channel_id").equals(channelId)) {
|
||||
hasMore = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMore) {
|
||||
notificationsInChannel.remove(groupId);
|
||||
} else {
|
||||
notificationsInChannel.put(groupId, notifications);
|
||||
notificationsInChannel.remove(channelId);
|
||||
}
|
||||
|
||||
saveNotificationsMap(context, notificationsInChannel);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearChannelNotifications(Context context, String channelId, String rootId, Boolean isCRTEnabled) {
|
||||
public static void clearChannelNotifications(Context context, String channelId) {
|
||||
if (!android.text.TextUtils.isEmpty(channelId)) {
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
|
||||
// rootId is available only when CRT is enabled & clearing the thread
|
||||
final boolean isClearThread = isCRTEnabled && !android.text.TextUtils.isEmpty(rootId);
|
||||
|
||||
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
|
||||
String groupId = isClearThread ? rootId : channelId;
|
||||
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
|
||||
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
|
||||
List<Integer> notifications = notificationsInChannel.get(channelId);
|
||||
if (notifications == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
notificationsInChannel.remove(groupId);
|
||||
notificationsInChannel.remove(channelId);
|
||||
saveNotificationsMap(context, notificationsInChannel);
|
||||
notifications.forEach(
|
||||
(notificationIdStr, post) -> notificationManager.cancel(Integer.valueOf(notificationIdStr))
|
||||
);
|
||||
|
||||
for (final Integer notificationId : notifications) {
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
notificationManager.cancel(notificationId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearAllNotifications(Context context) {
|
||||
if (context != null) {
|
||||
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(context);
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(context);
|
||||
notificationsInChannel.clear();
|
||||
saveNotificationsMap(context, notificationsInChannel);
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
@@ -145,8 +132,6 @@ public class CustomPushNotification extends PushNotification {
|
||||
final String ackId = initialData.getString("ack_id");
|
||||
final String postId = initialData.getString("post_id");
|
||||
final String channelId = initialData.getString("channel_id");
|
||||
final String rootId = initialData.getString("root_id");
|
||||
final boolean isCRTEnabled = initialData.getString("is_crt_enabled") != null && initialData.getString("is_crt_enabled").equals("true");
|
||||
final boolean isIdLoaded = initialData.getString("id_loaded") != null && initialData.getString("id_loaded").equals("true");
|
||||
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
|
||||
if (postId != null) {
|
||||
@@ -180,41 +165,24 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
if (type.equals(PUSH_TYPE_MESSAGE)) {
|
||||
if (channelId != null) {
|
||||
try {
|
||||
|
||||
JSONObject post = new JSONObject();
|
||||
if (!android.text.TextUtils.isEmpty(rootId)) {
|
||||
post.put("root_id", rootId);
|
||||
}
|
||||
if (!android.text.TextUtils.isEmpty(postId)) {
|
||||
post.put("post_id", postId);
|
||||
}
|
||||
|
||||
final Boolean isThreadNotification = isCRTEnabled && post.has("root_id");
|
||||
final String groupId = isThreadNotification ? rootId : channelId;
|
||||
|
||||
Map<String, Map<String, JSONObject>> notificationsInChannel = loadNotificationsMap(mContext);
|
||||
Map<String, JSONObject> notifications = notificationsInChannel.get(groupId);
|
||||
if (notifications == null) {
|
||||
notifications = Collections.synchronizedMap(new HashMap<String, JSONObject>());
|
||||
}
|
||||
|
||||
if (notifications.size() > 0) {
|
||||
createSummary = false;
|
||||
}
|
||||
|
||||
notifications.put(String.valueOf(notificationId), post);
|
||||
|
||||
if (createSummary) {
|
||||
// Add the summary notification id as well
|
||||
notifications.put(String.valueOf(notificationId + 1), new JSONObject());
|
||||
}
|
||||
|
||||
notificationsInChannel.put(groupId, notifications);
|
||||
saveNotificationsMap(mContext, notificationsInChannel);
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
|
||||
List<Integer> list = notificationsInChannel.get(channelId);
|
||||
if (list == null) {
|
||||
list = Collections.synchronizedList(new ArrayList(0));
|
||||
}
|
||||
|
||||
list.add(0, notificationId);
|
||||
if (list.size() > 1) {
|
||||
createSummary = false;
|
||||
}
|
||||
|
||||
if (createSummary) {
|
||||
// Add the summary notification id as well
|
||||
list.add(0, notificationId + 1);
|
||||
}
|
||||
|
||||
notificationsInChannel.put(channelId, list);
|
||||
saveNotificationsMap(mContext, notificationsInChannel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +190,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
break;
|
||||
case PUSH_TYPE_CLEAR:
|
||||
clearChannelNotifications(mContext, channelId, rootId, isCRTEnabled);
|
||||
clearChannelNotifications(mContext, channelId);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -237,11 +205,19 @@ public class CustomPushNotification extends PushNotification {
|
||||
|
||||
Bundle data = mNotificationProps.asBundle();
|
||||
final String channelId = data.getString("channel_id");
|
||||
final String rootId = data.getString("root_id");
|
||||
final Boolean isCRTEnabled = data.getBoolean("is_crt_enabled");
|
||||
final String postId = data.getString("post_id");
|
||||
Integer notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
|
||||
|
||||
if (postId != null) {
|
||||
notificationId = postId.hashCode();
|
||||
}
|
||||
|
||||
if (channelId != null) {
|
||||
clearChannelNotifications(mContext, channelId, rootId, isCRTEnabled);
|
||||
Map<String, List<Integer>> notificationsInChannel = loadNotificationsMap(mContext);
|
||||
List<Integer> notifications = notificationsInChannel.get(channelId);
|
||||
notifications.remove(notificationId);
|
||||
saveNotificationsMap(mContext, notificationsInChannel);
|
||||
clearChannelNotifications(mContext, channelId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +250,7 @@ public class CustomPushNotification extends PushNotification {
|
||||
mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext());
|
||||
}
|
||||
|
||||
private static void saveNotificationsMap(Context context, Map<String, Map<String, JSONObject>> inputMap) {
|
||||
private static void saveNotificationsMap(Context context, Map<String, List<Integer>> inputMap) {
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
|
||||
if (pSharedPref != null && context != null) {
|
||||
JSONObject json = new JSONObject(inputMap);
|
||||
@@ -286,41 +262,23 @@ public class CustomPushNotification extends PushNotification {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Structure
|
||||
*
|
||||
* {
|
||||
* channel_id1 | thread_id1: {
|
||||
* notification_id1: {
|
||||
* post_id: 'p1',
|
||||
* root_id: 'r1',
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
private static Map<String, Map<String, JSONObject>> loadNotificationsMap(Context context) {
|
||||
Map<String, Map<String, JSONObject>> outputMap = new HashMap<>();
|
||||
private static Map<String, List<Integer>> loadNotificationsMap(Context context) {
|
||||
Map<String, List<Integer>> outputMap = new HashMap<>();
|
||||
if (context != null) {
|
||||
SharedPreferences pSharedPref = context.getSharedPreferences(PUSH_NOTIFICATIONS, Context.MODE_PRIVATE);
|
||||
try {
|
||||
if (pSharedPref != null) {
|
||||
String jsonString = pSharedPref.getString(NOTIFICATIONS_IN_CHANNEL, (new JSONObject()).toString());
|
||||
JSONObject json = new JSONObject(jsonString);
|
||||
|
||||
// Can be a channel_id or thread_id
|
||||
Iterator<String> groupIdsItr = json.keys();
|
||||
while (groupIdsItr.hasNext()) {
|
||||
String groupId = groupIdsItr.next();
|
||||
JSONObject notificationsJSONObj = json.getJSONObject(groupId);
|
||||
Map<String, JSONObject> notifications = new HashMap<>();
|
||||
Iterator<String> notificationIdKeys = notificationsJSONObj.keys();
|
||||
while(notificationIdKeys.hasNext()) {
|
||||
String notificationId = notificationIdKeys.next();
|
||||
JSONObject post = notificationsJSONObj.getJSONObject(notificationId);
|
||||
notifications.put(notificationId, post);
|
||||
Iterator<String> keysItr = json.keys();
|
||||
while (keysItr.hasNext()) {
|
||||
String key = keysItr.next();
|
||||
JSONArray array = json.getJSONArray(key);
|
||||
List<Integer> values = new ArrayList<>();
|
||||
for (int i = 0; i < array.length(); ++i) {
|
||||
values.add(array.getInt(i));
|
||||
}
|
||||
outputMap.put(groupId, notifications);
|
||||
outputMap.put(key, values);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -98,16 +98,6 @@ public class CustomPushNotificationHelper {
|
||||
userInfoBundle = new Bundle();
|
||||
}
|
||||
|
||||
String postId = bundle.getString("post_id");
|
||||
if (postId != null) {
|
||||
userInfoBundle.putString("post_id", postId);
|
||||
}
|
||||
|
||||
String rootId = bundle.getString("root_id");
|
||||
if (rootId != null) {
|
||||
userInfoBundle.putString("root_id", rootId);
|
||||
}
|
||||
|
||||
String channelId = bundle.getString("channel_id");
|
||||
if (channelId != null) {
|
||||
userInfoBundle.putString("channel_id", channelId);
|
||||
@@ -155,17 +145,13 @@ public class CustomPushNotificationHelper {
|
||||
|
||||
String channelId = bundle.getString("channel_id");
|
||||
String postId = bundle.getString("post_id");
|
||||
String rootId = bundle.getString("root_id");
|
||||
int notificationId = postId != null ? postId.hashCode() : MESSAGE_NOTIFICATION_ID;
|
||||
NotificationPreferences notificationPreferences = NotificationPreferences.getInstance(context);
|
||||
|
||||
Boolean is_crt_enabled = bundle.getString("is_crt_enabled") != null && bundle.getString("is_crt_enabled").equals("true");
|
||||
String groupId = is_crt_enabled && !android.text.TextUtils.isEmpty(rootId) ? rootId : channelId;
|
||||
|
||||
addNotificationExtras(notification, bundle);
|
||||
setNotificationIcons(context, notification, bundle);
|
||||
setNotificationMessagingStyle(context, notification, bundle);
|
||||
setNotificationGroup(notification, groupId, createSummary);
|
||||
setNotificationGroup(notification, channelId, createSummary);
|
||||
setNotificationBadgeType(notification);
|
||||
setNotificationSound(notification, notificationPreferences);
|
||||
setNotificationVibrate(notification, notificationPreferences);
|
||||
|
||||
@@ -19,9 +19,6 @@ public class NotificationDismissService extends IntentService {
|
||||
final Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
|
||||
final String channelId = bundle.getString("channel_id");
|
||||
final String postId = bundle.getString("post_id");
|
||||
final String rootId = bundle.getString("root_id");
|
||||
final Boolean isCRTEnabled = bundle.getString("is_crt_enabled") != null && bundle.getString("is_crt_enabled").equals("true");
|
||||
|
||||
int notificationId = CustomPushNotificationHelper.MESSAGE_NOTIFICATION_ID;
|
||||
if (postId != null) {
|
||||
notificationId = postId.hashCode();
|
||||
@@ -29,7 +26,7 @@ public class NotificationDismissService extends IntentService {
|
||||
notificationId = channelId.hashCode();
|
||||
}
|
||||
|
||||
CustomPushNotification.cancelNotification(context, channelId, rootId, notificationId, isCRTEnabled);
|
||||
CustomPushNotification.cancelNotification(context, channelId, notificationId);
|
||||
Log.i("ReactNative", "Dismiss notification");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,10 +118,6 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
|
||||
WritableMap map = Arguments.createMap();
|
||||
Notification n = sbn.getNotification();
|
||||
Bundle bundle = n.extras;
|
||||
String postId = bundle.getString("post_id");
|
||||
map.putString("post_id", postId);
|
||||
String rootId = bundle.getString("root_id");
|
||||
map.putString("root_id", rootId);
|
||||
String channelId = bundle.getString("channel_id");
|
||||
map.putString("channel_id", channelId);
|
||||
result.pushMap(map);
|
||||
@@ -130,9 +126,8 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void removeDeliveredNotifications(String channelId, String rootId, Boolean isCRTEnabled) {
|
||||
public void removeDeliveredNotifications(String channelId) {
|
||||
final Context context = mApplication.getApplicationContext();
|
||||
CustomPushNotification.clearChannelNotifications(context, channelId, rootId, isCRTEnabled);
|
||||
CustomPushNotification.clearChannelNotifications(context, channelId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ buildscript {
|
||||
kotlinVersion = "1.5.30"
|
||||
firebaseVersion = "21.0.0"
|
||||
RNNKotlinVersion = kotlinVersion
|
||||
ndkVersion = "21.4.7075529"
|
||||
ndkVersion = "20.1.5948944"
|
||||
|
||||
}
|
||||
repositories {
|
||||
@@ -20,7 +20,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.2.2'
|
||||
classpath 'com.android.tools.build:gradle:4.2.1'
|
||||
classpath 'com.google.gms:google-services:4.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
|
||||
@@ -52,6 +52,9 @@ allprojects {
|
||||
maven {
|
||||
url "https://www.jitpack.io"
|
||||
}
|
||||
maven {
|
||||
url ("https://dl.bintray.com/rudderstack/rudderstack")
|
||||
}
|
||||
maven {
|
||||
url "$rootDir/../node_modules/detox/Detox-android"
|
||||
}
|
||||
|
||||
@@ -30,4 +30,4 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.99.0
|
||||
FLIPPER_VERSION=0.93.0
|
||||
|
||||
@@ -390,7 +390,7 @@ export function markAsViewedAndReadBatch(state, channelId, prevChannelId = '', m
|
||||
type: ChannelTypes.SET_UNREAD_MSG_COUNT,
|
||||
data: {
|
||||
channelId,
|
||||
count: isCollapsedThreadsEnabled(state) ? unreadMessageCountRoot : unreadMessageCount,
|
||||
count: unreadMessageCount,
|
||||
},
|
||||
}, {
|
||||
type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
|
||||
|
||||
@@ -19,11 +19,6 @@ export function setCustomStatus(customStatus: UserCustomStatus): ActionFunc {
|
||||
user.props.customStatus = JSON.stringify(customStatus);
|
||||
dispatch({type: UserTypes.RECEIVED_ME, data: user});
|
||||
|
||||
// Server does not like empty 'expires_at' string.
|
||||
if (!customStatus.expires_at) {
|
||||
delete customStatus.expires_at;
|
||||
}
|
||||
|
||||
try {
|
||||
await Client4.updateCustomStatus(customStatus);
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,16 +2,6 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import {ViewTypes} from '@constants';
|
||||
|
||||
export function updateThreadLastViewedAt(threadId: string, lastViewedAt: number) {
|
||||
return {
|
||||
type: ViewTypes.THREAD_LAST_VIEWED_AT,
|
||||
data: {
|
||||
threadId,
|
||||
lastViewedAt,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const handleViewingGlobalThreadsScreen = () => (
|
||||
{
|
||||
type: ViewTypes.VIEWING_GLOBAL_THREADS_SCREEN,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {getThreads} from '@mm-redux/actions/threads';
|
||||
import {getProfilesByIds, getStatusesByIds} from '@mm-redux/actions/users';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getCurrentChannelId, getCurrentChannelStats} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig, getFeatureFlagValue} from '@mm-redux/selectors/entities/general';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getPostIdsInChannel} from '@mm-redux/selectors/entities/posts';
|
||||
import {isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
@@ -23,20 +23,6 @@ import {TeamMembership} from '@mm-redux/types/teams';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {removeUserFromList} from '@mm-redux/utils/user_utils';
|
||||
import {loadCalls} from '@mmproducts/calls/store/actions/calls';
|
||||
import {
|
||||
handleCallStarted,
|
||||
handleCallUserConnected,
|
||||
handleCallUserDisconnected,
|
||||
handleCallUserMuted,
|
||||
handleCallUserUnmuted,
|
||||
handleCallUserVoiceOn,
|
||||
handleCallUserVoiceOff,
|
||||
handleCallChannelEnabled,
|
||||
handleCallChannelDisabled,
|
||||
handleCallScreenOn,
|
||||
handleCallScreenOff,
|
||||
} from '@mmproducts/calls/store/actions/websockets';
|
||||
import {getChannelSinceValue} from '@utils/channels';
|
||||
import websocketClient from '@websocket';
|
||||
|
||||
@@ -161,10 +147,6 @@ export function doReconnect(now: number) {
|
||||
const {data: me}: any = await dispatch(loadMe(null, null, true));
|
||||
|
||||
if (!me.error) {
|
||||
if (getFeatureFlagValue(getState(), 'CallsMobile') === 'true') {
|
||||
dispatch(loadCalls());
|
||||
}
|
||||
|
||||
const roles = [];
|
||||
|
||||
if (me.roles?.length) {
|
||||
@@ -343,7 +325,7 @@ function handleClose(connectFailCount: number) {
|
||||
}
|
||||
|
||||
function handleEvent(msg: WebSocketMessage) {
|
||||
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
switch (msg.event) {
|
||||
case WebsocketEvents.POSTED:
|
||||
case WebsocketEvents.EPHEMERAL_MESSAGE:
|
||||
@@ -439,33 +421,6 @@ function handleEvent(msg: WebSocketMessage) {
|
||||
return dispatch(handleSidebarCategoryOrderUpdated(msg));
|
||||
}
|
||||
|
||||
if (getFeatureFlagValue(getState(), 'CallsMobile') === 'true') {
|
||||
switch (msg.event) {
|
||||
case WebsocketEvents.CALLS_CHANNEL_ENABLED:
|
||||
return dispatch(handleCallChannelEnabled(msg));
|
||||
case WebsocketEvents.CALLS_CHANNEL_DISABLED:
|
||||
return dispatch(handleCallChannelDisabled(msg));
|
||||
case WebsocketEvents.CALLS_USER_CONNECTED:
|
||||
return dispatch(handleCallUserConnected(msg));
|
||||
case WebsocketEvents.CALLS_USER_DISCONNECTED:
|
||||
return dispatch(handleCallUserDisconnected(msg));
|
||||
case WebsocketEvents.CALLS_USER_MUTED:
|
||||
return dispatch(handleCallUserMuted(msg));
|
||||
case WebsocketEvents.CALLS_USER_UNMUTED:
|
||||
return dispatch(handleCallUserUnmuted(msg));
|
||||
case WebsocketEvents.CALLS_USER_VOICE_ON:
|
||||
return dispatch(handleCallUserVoiceOn(msg));
|
||||
case WebsocketEvents.CALLS_USER_VOICE_OFF:
|
||||
return dispatch(handleCallUserVoiceOff(msg));
|
||||
case WebsocketEvents.CALLS_CALL_START:
|
||||
return dispatch(handleCallStarted(msg));
|
||||
case WebsocketEvents.CALLS_SCREEN_ON:
|
||||
return dispatch(handleCallScreenOn(msg));
|
||||
case WebsocketEvents.CALLS_SCREEN_OFF:
|
||||
return dispatch(handleCallScreenOff(msg));
|
||||
}
|
||||
}
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {updateThreadLastViewedAt} from '@actions/views/threads';
|
||||
import {handleThreadArrived, handleReadChanged, handleAllMarkedRead, handleFollowChanged, getThread as fetchThread} from '@mm-redux/actions/threads';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/common';
|
||||
import {getSelectedPost} from '@mm-redux/selectors/entities/posts';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getThread} from '@mm-redux/selectors/entities/threads';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc} from '@mm-redux/types/actions';
|
||||
import {ActionResult, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleThreadUpdated(msg: WebSocketMessage) {
|
||||
@@ -31,31 +27,21 @@ export function handleThreadReadChanged(msg: WebSocketMessage) {
|
||||
const thread = getThread(state, msg.data.thread_id);
|
||||
|
||||
// Mark only following threads as read.
|
||||
if (thread) {
|
||||
const actions: GenericAction[] = [];
|
||||
const selectedPost = getSelectedPost(state);
|
||||
if (selectedPost?.id !== thread.id) {
|
||||
actions.push(updateThreadLastViewedAt(thread.id, msg.data.timestamp));
|
||||
}
|
||||
if (thread.is_following) {
|
||||
actions.push(
|
||||
handleReadChanged(
|
||||
msg.data.thread_id,
|
||||
msg.broadcast.team_id,
|
||||
msg.data.channel_id,
|
||||
{
|
||||
lastViewedAt: msg.data.timestamp,
|
||||
prevUnreadMentions: thread.unread_mentions,
|
||||
newUnreadMentions: msg.data.unread_mentions,
|
||||
prevUnreadReplies: thread.unread_replies,
|
||||
newUnreadReplies: msg.data.unread_replies,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions));
|
||||
}
|
||||
if (thread?.is_following) {
|
||||
dispatch(
|
||||
handleReadChanged(
|
||||
msg.data.thread_id,
|
||||
msg.broadcast.team_id,
|
||||
msg.data.channel_id,
|
||||
{
|
||||
lastViewedAt: msg.data.timestamp,
|
||||
prevUnreadMentions: thread.unread_mentions,
|
||||
newUnreadMentions: msg.data.unread_mentions,
|
||||
prevUnreadReplies: thread.unread_replies,
|
||||
newUnreadReplies: msg.data.unread_replies,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
dispatch(handleAllMarkedRead(msg.broadcast.team_id));
|
||||
|
||||
@@ -290,10 +290,6 @@ export default class ClientBase {
|
||||
return `${this.url}/plugins/com.mattermost.apps`;
|
||||
}
|
||||
|
||||
getCallsRoute() {
|
||||
return `${this.url}/plugins/com.mattermost.calls`;
|
||||
}
|
||||
|
||||
// Client Helpers
|
||||
handleRedirectProtocol = (url: string, response: RNFetchBlobFetchRepsonse) => {
|
||||
const serverUrl = this.getUrl();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import ClientCalls, {ClientCallsMix} from '@mmproducts/calls/client/rest';
|
||||
import mix from '@utils/mix';
|
||||
|
||||
import ClientApps, {ClientAppsMix} from './apps';
|
||||
@@ -35,8 +34,7 @@ interface Client extends ClientBase,
|
||||
ClientSharedChannelsMix,
|
||||
ClientTeamsMix,
|
||||
ClientTosMix,
|
||||
ClientUsersMix,
|
||||
ClientCallsMix
|
||||
ClientUsersMix
|
||||
{}
|
||||
|
||||
class Client extends mix(ClientBase).with(
|
||||
@@ -54,7 +52,6 @@ class Client extends mix(ClientBase).with(
|
||||
ClientTeams,
|
||||
ClientTos,
|
||||
ClientUsers,
|
||||
ClientCalls,
|
||||
) {}
|
||||
|
||||
const Client4 = new Client();
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FormattedRelativeTime should match snapshot 1`] = `
|
||||
<Text>
|
||||
a few seconds ago
|
||||
</Text>
|
||||
`;
|
||||
@@ -113,6 +113,7 @@ export default class AtMention extends React.PureComponent {
|
||||
|
||||
BottomSheet.showBottomSheetWithOptions({
|
||||
options: [actionText, cancelText],
|
||||
cancelButtonIndex: 1,
|
||||
}, (value) => {
|
||||
if (value !== 1) {
|
||||
this.handleCopyMention();
|
||||
|
||||
@@ -66,7 +66,7 @@ const GroupMentionItem = (props) => {
|
||||
>
|
||||
<View style={style.rowPicture}>
|
||||
<CompassIcon
|
||||
name='account-multiple-outline'
|
||||
name='account-group-outline'
|
||||
style={style.rowIcon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -4685,6 +4685,7 @@ exports[`components/autocomplete/emoji_suggestion should match snapshot 2`] = `
|
||||
keyExtractor={[Function]}
|
||||
keyboardShouldPersistTaps="always"
|
||||
nestedScrollEnabled={false}
|
||||
numColumns={1}
|
||||
pageSize={10}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
|
||||
@@ -31,6 +31,7 @@ exports[`components/autocomplete/slash_suggestion should match snapshot 1`] = `
|
||||
keyExtractor={[Function]}
|
||||
keyboardShouldPersistTaps="always"
|
||||
nestedScrollEnabled={false}
|
||||
numColumns={1}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
style={
|
||||
|
||||
@@ -16,6 +16,7 @@ exports[`components/autocomplete/app_slash_suggestion should match snapshot 1`]
|
||||
keyExtractor={[Function]}
|
||||
keyboardShouldPersistTaps="always"
|
||||
nestedScrollEnabled={false}
|
||||
numColumns={1}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
style={
|
||||
|
||||
@@ -155,15 +155,13 @@ const SlashSuggestionItem = (props: Props) => {
|
||||
</View>
|
||||
<View style={style.suggestionContainer}>
|
||||
<Text style={style.suggestionName}>{`${suggestionText}`}</Text>
|
||||
{Boolean(description) &&
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={style.suggestionDescription}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
}
|
||||
<Text
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
style={style.suggestionDescription}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithFeedback>
|
||||
|
||||
@@ -48,7 +48,7 @@ export default class SpecialMentionItem extends PureComponent {
|
||||
<View style={style.row}>
|
||||
<View style={style.rowPicture}>
|
||||
<CompassIcon
|
||||
name='account-multiple-outline'
|
||||
name='account-group-outline'
|
||||
style={style.rowIcon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {PureComponent} from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Text, View, Platform} from 'react-native';
|
||||
import {Text, View} from 'react-native';
|
||||
|
||||
import {goToScreen} from '@actions/navigation';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
@@ -11,46 +12,33 @@ import FormattedText from '@components/formatted_text';
|
||||
import Markdown from '@components/markdown';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {ActionResult} from '@mm-redux/types/actions';
|
||||
import {Channel} from '@mm-redux/types/channels';
|
||||
import {DialogOption} from '@mm-redux/types/integrations';
|
||||
import {Theme} from '@mm-redux/types/theme';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
||||
type Selection = DialogOption | Channel | UserProfile | DialogOption[] | Channel[] | UserProfile[];
|
||||
|
||||
type Props = {
|
||||
actions: {
|
||||
setAutocompleteSelector: (dataSource: any, onSelect: any, options: any, getDynamicOptions: any) => Promise<ActionResult>;
|
||||
export default class AutocompleteSelector extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
setAutocompleteSelector: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
getDynamicOptions: PropTypes.func,
|
||||
label: PropTypes.string,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
dataSource: PropTypes.string,
|
||||
options: PropTypes.arrayOf(PropTypes.object),
|
||||
selected: PropTypes.object,
|
||||
optional: PropTypes.bool,
|
||||
showRequiredAsterisk: PropTypes.bool,
|
||||
teammateNameDisplay: PropTypes.string,
|
||||
theme: PropTypes.object.isRequired,
|
||||
onSelected: PropTypes.func,
|
||||
helpText: PropTypes.node,
|
||||
errorText: PropTypes.node,
|
||||
roundedBorders: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
getDynamicOptions?: (term: string) => Promise<ActionResult>;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
dataSource?: string;
|
||||
options?: DialogOption[];
|
||||
selected?: DialogOption | DialogOption[];
|
||||
optional?: boolean;
|
||||
showRequiredAsterisk?: boolean;
|
||||
teammateNameDisplay?: string;
|
||||
theme: Theme;
|
||||
onSelected?: ((item: DialogOption) => void) | ((item: DialogOption[]) => void);
|
||||
helpText?: string;
|
||||
errorText?: string;
|
||||
roundedBorders?: boolean;
|
||||
disabled?: boolean;
|
||||
isMultiselect?: boolean;
|
||||
}
|
||||
|
||||
type State = {
|
||||
selectedText: string;
|
||||
selected?: DialogOption | DialogOption[];
|
||||
}
|
||||
|
||||
export default class AutocompleteSelector extends PureComponent<Props, State> {
|
||||
static contextTypes = {
|
||||
intl: intlShape,
|
||||
};
|
||||
@@ -61,45 +49,26 @@ export default class AutocompleteSelector extends PureComponent<Props, State> {
|
||||
roundedBorders: true,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selectedText: '',
|
||||
selectedText: null,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Props, state: State) {
|
||||
if (!props.selected || props.selected === state.selected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!props.isMultiselect) {
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (props.selected && props.selected !== state.selected) {
|
||||
return {
|
||||
selectedText: (props.selected as DialogOption).text,
|
||||
selectedText: props.selected.text,
|
||||
selected: props.selected,
|
||||
};
|
||||
}
|
||||
|
||||
const options = props.selected as DialogOption[];
|
||||
let selectedText = '';
|
||||
const selected: DialogOption[] = [];
|
||||
|
||||
options.forEach((option) => {
|
||||
if (selectedText !== '') {
|
||||
selectedText += ', ';
|
||||
}
|
||||
selectedText += option.text;
|
||||
selected.push(option);
|
||||
});
|
||||
|
||||
return {
|
||||
selectedText,
|
||||
selected,
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
handleSelect = (selected: Selection) => {
|
||||
handleSelect = (selected) => {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
@@ -109,113 +78,34 @@ export default class AutocompleteSelector extends PureComponent<Props, State> {
|
||||
teammateNameDisplay,
|
||||
} = this.props;
|
||||
|
||||
if (!this.props.isMultiselect) {
|
||||
let selectedText: string;
|
||||
let selectedValue: string;
|
||||
switch (dataSource) {
|
||||
case ViewTypes.DATA_SOURCE_USERS: {
|
||||
const typedSelected = selected as UserProfile;
|
||||
selectedText = displayUsername(typedSelected, teammateNameDisplay || '');
|
||||
selectedValue = typedSelected.id;
|
||||
break;
|
||||
}
|
||||
case ViewTypes.DATA_SOURCE_CHANNELS: {
|
||||
const typedSelected = selected as Channel;
|
||||
selectedText = typedSelected.display_name;
|
||||
selectedValue = typedSelected.id;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const typedSelected = selected as DialogOption;
|
||||
selectedText = typedSelected.text;
|
||||
selectedValue = typedSelected.value;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({selectedText});
|
||||
|
||||
if (this.props.onSelected) {
|
||||
(this.props.onSelected as (opt: DialogOption) => void)({text: selectedText, value: selectedValue});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedText = '';
|
||||
const selectedOptions: DialogOption[] = [];
|
||||
switch (dataSource) {
|
||||
case ViewTypes.DATA_SOURCE_USERS: {
|
||||
const typedSelected = selected as UserProfile[];
|
||||
typedSelected.forEach((option) => {
|
||||
if (selectedText !== '') {
|
||||
selectedText += ', ';
|
||||
}
|
||||
const text = displayUsername(option, teammateNameDisplay || '');
|
||||
selectedText += text;
|
||||
selectedOptions.push({text, value: option.id});
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ViewTypes.DATA_SOURCE_CHANNELS: {
|
||||
const typedSelected = selected as Channel[];
|
||||
typedSelected.forEach((option) => {
|
||||
if (selectedText !== '') {
|
||||
selectedText += ', ';
|
||||
}
|
||||
const text = option.display_name;
|
||||
selectedText += text;
|
||||
selectedOptions.push({text, value: option.id});
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const typedSelected = selected as DialogOption[];
|
||||
typedSelected.forEach((option) => {
|
||||
if (selectedText !== '') {
|
||||
selectedText += ', ';
|
||||
}
|
||||
selectedText += option.text;
|
||||
selectedOptions.push(option);
|
||||
});
|
||||
break;
|
||||
}
|
||||
let selectedText;
|
||||
let selectedValue;
|
||||
if (dataSource === ViewTypes.DATA_SOURCE_USERS) {
|
||||
selectedText = displayUsername(selected, teammateNameDisplay);
|
||||
selectedValue = selected.id;
|
||||
} else if (dataSource === ViewTypes.DATA_SOURCE_CHANNELS) {
|
||||
selectedText = selected.display_name;
|
||||
selectedValue = selected.id;
|
||||
} else {
|
||||
selectedText = selected.text;
|
||||
selectedValue = selected.value;
|
||||
}
|
||||
|
||||
this.setState({selectedText});
|
||||
|
||||
if (this.props.onSelected) {
|
||||
(this.props.onSelected as (opt: DialogOption[]) => void)(selectedOptions);
|
||||
this.props.onSelected({text: selectedText, value: selectedValue});
|
||||
}
|
||||
};
|
||||
|
||||
goToSelectorScreen = preventDoubleTap(async () => {
|
||||
const closeButton = await CompassIcon.getImageSource(Platform.select({ios: 'arrow-back-ios', default: 'arrow-left'}), 24, this.props.theme.sidebarHeaderTextColor);
|
||||
|
||||
goToSelectorScreen = preventDoubleTap(() => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {actions, dataSource, options, placeholder, getDynamicOptions, theme} = this.props;
|
||||
const {actions, dataSource, options, placeholder, getDynamicOptions} = this.props;
|
||||
const screen = 'SelectorScreen';
|
||||
const title = placeholder || formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
|
||||
const buttonName = formatMessage({id: 'mobile.forms.select.done', defaultMessage: 'Done'});
|
||||
|
||||
actions.setAutocompleteSelector(dataSource, this.handleSelect, options, getDynamicOptions);
|
||||
let screenOptions = {};
|
||||
if (this.props.isMultiselect) {
|
||||
screenOptions = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: 'close-dialog',
|
||||
icon: closeButton,
|
||||
}],
|
||||
rightButtons: [{
|
||||
id: 'submit-form',
|
||||
showAsAction: 'always',
|
||||
text: buttonName,
|
||||
}],
|
||||
leftButtonColor: theme.sidebarHeaderTextColor,
|
||||
rightButtonColor: theme.sidebarHeaderTextColor,
|
||||
},
|
||||
};
|
||||
}
|
||||
goToScreen(screen, title, {isMultiselect: this.props.isMultiselect, selected: this.state.selected}, screenOptions);
|
||||
goToScreen(screen, title);
|
||||
});
|
||||
|
||||
render() {
|
||||
@@ -236,8 +126,6 @@ export default class AutocompleteSelector extends PureComponent<Props, State> {
|
||||
const textStyles = getMarkdownTextStyles(theme);
|
||||
const blockStyles = getMarkdownBlockStyles(theme);
|
||||
|
||||
const chevron = Platform.select({ios: 'chevron-right', default: 'chevron-down'});
|
||||
|
||||
let text = placeholder || intl.formatMessage({id: 'mobile.action_menu.select', defaultMessage: 'Select an option'});
|
||||
let selectedStyle = style.dropdownPlaceholder;
|
||||
|
||||
@@ -327,8 +215,8 @@ export default class AutocompleteSelector extends PureComponent<Props, State> {
|
||||
{text}
|
||||
</Text>
|
||||
<CompassIcon
|
||||
name={chevron}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.32)}
|
||||
name='chevron-down'
|
||||
color={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
style={style.icon}
|
||||
/>
|
||||
</View>
|
||||
@@ -340,7 +228,7 @@ export default class AutocompleteSelector extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
const input = {
|
||||
borderWidth: 1,
|
||||
borderColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
@@ -375,9 +263,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
},
|
||||
icon: {
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
top: 13,
|
||||
right: 12,
|
||||
fontSize: 28,
|
||||
},
|
||||
labelContainer: {
|
||||
flexDirection: 'row',
|
||||
@@ -2,29 +2,23 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {setAutocompleteSelector} from '@actions/views/post';
|
||||
import {getTeammateNameDisplaySetting, getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
|
||||
import AutocompleteSelector from './autocomplete_selector';
|
||||
|
||||
import type {Action, ActionResult, GenericAction} from '@mm-redux/types/actions';
|
||||
import type {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
teammateNameDisplay: getTeammateNameDisplaySetting(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
type Actions = {
|
||||
setAutocompleteSelector: (dataSource: any, onSelect: any, options: any, getDynamicOptions: any) => Promise<ActionResult>;
|
||||
}
|
||||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<Action>, Actions>({
|
||||
actions: bindActionCreators({
|
||||
setAutocompleteSelector,
|
||||
}, dispatch),
|
||||
};
|
||||
@@ -86,7 +86,6 @@ export interface AvatarsProps {
|
||||
breakAt?: number;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
theme: Theme;
|
||||
listTitle?: JSX.Element;
|
||||
}
|
||||
|
||||
export default class Avatars extends PureComponent<AvatarsProps> {
|
||||
@@ -95,12 +94,11 @@ export default class Avatars extends PureComponent<AvatarsProps> {
|
||||
};
|
||||
|
||||
showParticipantsList = () => {
|
||||
const {userIds, listTitle} = this.props;
|
||||
const {userIds} = this.props;
|
||||
|
||||
const screen = 'ParticipantsList';
|
||||
const passProps = {
|
||||
userIds,
|
||||
listTitle,
|
||||
};
|
||||
|
||||
showModalOverCurrentContext(screen, passProps);
|
||||
|
||||
@@ -25,6 +25,7 @@ exports[`CustomList should match snapshot with FlatList 1`] = `
|
||||
keyboardDismissMode="on-drag"
|
||||
keyboardShouldPersistTaps="always"
|
||||
maxToRenderPerBatch={16}
|
||||
numColumns={1}
|
||||
onLayout={[Function]}
|
||||
onScroll={[Function]}
|
||||
removeClippedSubviews={true}
|
||||
|
||||
@@ -52,35 +52,33 @@ export default class ChannelListRow extends React.PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.outerContainer}>
|
||||
<CustomListRow
|
||||
id={this.props.id}
|
||||
onPress={this.props.onPress ? this.onPress : null}
|
||||
enabled={this.props.enabled}
|
||||
selectable={this.props.selectable}
|
||||
selected={this.props.selected}
|
||||
testID={testID}
|
||||
<CustomListRow
|
||||
id={this.props.id}
|
||||
onPress={this.props.onPress ? this.onPress : null}
|
||||
enabled={this.props.enabled}
|
||||
selectable={this.props.selectable}
|
||||
selected={this.props.selected}
|
||||
testID={testID}
|
||||
>
|
||||
<View
|
||||
style={style.container}
|
||||
testID={itemTestID}
|
||||
>
|
||||
<View
|
||||
style={style.container}
|
||||
testID={itemTestID}
|
||||
>
|
||||
<View style={style.titleContainer}>
|
||||
<CompassIcon
|
||||
name={icon}
|
||||
style={style.icon}
|
||||
/>
|
||||
<Text
|
||||
style={style.displayName}
|
||||
testID={channelDisplayNameTestID}
|
||||
>
|
||||
{this.props.channel.display_name}
|
||||
</Text>
|
||||
</View>
|
||||
{purpose}
|
||||
<View style={style.titleContainer}>
|
||||
<CompassIcon
|
||||
name={icon}
|
||||
style={style.icon}
|
||||
/>
|
||||
<Text
|
||||
style={style.displayName}
|
||||
testID={channelDisplayNameTestID}
|
||||
>
|
||||
{this.props.channel.display_name}
|
||||
</Text>
|
||||
</View>
|
||||
</CustomListRow>
|
||||
</View>
|
||||
{purpose}
|
||||
</View>
|
||||
</CustomListRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -103,12 +101,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
},
|
||||
outerContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 15,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
purpose: {
|
||||
marginTop: 7,
|
||||
|
||||
@@ -42,23 +42,21 @@ export default class OptionListRow extends React.PureComponent {
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<CustomListRow
|
||||
id={value}
|
||||
onPress={this.onPress}
|
||||
enabled={enabled}
|
||||
selectable={selectable}
|
||||
selected={selected}
|
||||
>
|
||||
<View style={style.textContainer}>
|
||||
<View>
|
||||
<Text style={style.optionText}>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
<CustomListRow
|
||||
id={value}
|
||||
onPress={this.onPress}
|
||||
enabled={enabled}
|
||||
selectable={selectable}
|
||||
selected={selected}
|
||||
>
|
||||
<View style={style.textContainer}>
|
||||
<View>
|
||||
<Text style={style.optionText}>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
</CustomListRow>
|
||||
</View>
|
||||
</View>
|
||||
</CustomListRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ exports[`UserListRow should match snapshot 1`] = `
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginHorizontal": 10,
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -142,8 +142,8 @@ exports[`UserListRow should match snapshot for currentUser with (you) populated
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginHorizontal": 10,
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -278,8 +278,8 @@ exports[`UserListRow should match snapshot for deactivated user 1`] = `
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginHorizontal": 10,
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -427,8 +427,8 @@ exports[`UserListRow should match snapshot for guest user 1`] = `
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginHorizontal": 10,
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -563,8 +563,8 @@ exports[`UserListRow should match snapshot for remote user 1`] = `
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginHorizontal": 10,
|
||||
"overflow": "hidden",
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
@@ -165,7 +165,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 15,
|
||||
marginHorizontal: 10,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
profileContainer: {
|
||||
|
||||
@@ -74,7 +74,6 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
allowFontScaling={true}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
disableFullscreenUI={true}
|
||||
@@ -141,7 +140,6 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
allowFontScaling={true}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
@@ -231,7 +229,6 @@ exports[`EditChannelInfo should match snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
allowFontScaling={true}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
|
||||
@@ -279,7 +279,6 @@ export default class EditChannelInfo extends PureComponent {
|
||||
onPress={() => {
|
||||
this.onTypeSelect(General.OPEN_CHANNEL);
|
||||
}}
|
||||
testID='edit_channel_info.type.public.action'
|
||||
>
|
||||
<FormattedText
|
||||
style={style.touchableText}
|
||||
@@ -306,7 +305,6 @@ export default class EditChannelInfo extends PureComponent {
|
||||
onPress={() => {
|
||||
this.onTypeSelect(General.PRIVATE_CHANNEL);
|
||||
}}
|
||||
testID='edit_channel_info.type.private.action'
|
||||
>
|
||||
<FormattedText
|
||||
style={style.touchableText}
|
||||
@@ -336,7 +334,6 @@ export default class EditChannelInfo extends PureComponent {
|
||||
</View>
|
||||
<View style={style.inputContainer}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
allowFontScaling={true}
|
||||
testID='edit_channel_info.name.input'
|
||||
ref={this.nameInput}
|
||||
value={displayName}
|
||||
@@ -367,7 +364,6 @@ export default class EditChannelInfo extends PureComponent {
|
||||
</View>
|
||||
<View style={style.inputContainer}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
allowFontScaling={true}
|
||||
testID='edit_channel_info.purpose.input'
|
||||
ref={this.purposeInput}
|
||||
value={purpose}
|
||||
@@ -411,7 +407,6 @@ export default class EditChannelInfo extends PureComponent {
|
||||
</View>
|
||||
<View style={style.inputContainer}>
|
||||
<TextInputWithLocalizedPlaceholder
|
||||
allowFontScaling={true}
|
||||
testID='edit_channel_info.header.input'
|
||||
ref={this.headerInput}
|
||||
value={header}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
|
||||
import FormattedRelativeTime from './formatted_relative_time';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useEffect: (f) => f(),
|
||||
}));
|
||||
|
||||
describe('FormattedRelativeTime', () => {
|
||||
const baseProps = {
|
||||
value: moment.now() - 15000,
|
||||
updateIntervalInSeconds: 10000,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(<FormattedRelativeTime {...baseProps}/>);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match string in the past', () => {
|
||||
const props = {...baseProps, value: moment.now() - ((10 * 60 * 60 * 1000) + (30 * 60 * 1000) + (25 * 1000) + 500)};
|
||||
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
|
||||
|
||||
expect(wrapper.getElement().props.children).toBe('11 hours ago');
|
||||
});
|
||||
|
||||
test('should match string in the future', () => {
|
||||
const props = {...baseProps, value: moment.now() + 15500};
|
||||
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
|
||||
|
||||
expect(wrapper.getElement().props.children).toBe('in a few seconds');
|
||||
});
|
||||
|
||||
test('should re-render after updateIntervalInSeconds', () => {
|
||||
jest.useFakeTimers();
|
||||
const props = {...baseProps, value: moment.now(), updateIntervalInSeconds: 120};
|
||||
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
|
||||
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
|
||||
jest.advanceTimersByTime(60000);
|
||||
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
|
||||
jest.advanceTimersByTime(120000);
|
||||
expect(wrapper.getElement().props.children).toBe('2 minutes ago');
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('should not re-render if updateIntervalInSeconds is not passed', () => {
|
||||
jest.useFakeTimers();
|
||||
const props = {value: baseProps.value};
|
||||
const wrapper = shallow(<FormattedRelativeTime {...props}/>);
|
||||
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
|
||||
jest.advanceTimersByTime(120000000000);
|
||||
expect(wrapper.getElement().props.children).toBe('a few seconds ago');
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Text, TextProps} from 'react-native';
|
||||
|
||||
import type {UserTimezone} from '@mm-redux/types/users';
|
||||
|
||||
type FormattedRelativeTimeProps = TextProps & {
|
||||
timezone?: UserTimezone | string;
|
||||
value: number | string | Date;
|
||||
updateIntervalInSeconds?: number;
|
||||
}
|
||||
|
||||
const FormattedRelativeTime = ({timezone, value, updateIntervalInSeconds, ...props}: FormattedRelativeTimeProps) => {
|
||||
const getFormattedRelativeTime = () => {
|
||||
let zone = timezone;
|
||||
if (typeof timezone === 'object') {
|
||||
zone = timezone.useAutomaticTimezone ? timezone.automaticTimezone : timezone.manualTimezone;
|
||||
}
|
||||
|
||||
return timezone ? moment.tz(value, zone as string).fromNow() : moment(value).fromNow();
|
||||
};
|
||||
|
||||
const [formattedTime, setFormattedTime] = useState(getFormattedRelativeTime());
|
||||
useEffect(() => {
|
||||
if (updateIntervalInSeconds) {
|
||||
const interval = setInterval(() => setFormattedTime(getFormattedRelativeTime()), updateIntervalInSeconds * 1000);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}
|
||||
return () => null;
|
||||
}, [updateIntervalInSeconds]);
|
||||
|
||||
return (
|
||||
<Text {...props}>
|
||||
{formattedTime}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormattedRelativeTime;
|
||||
@@ -5,14 +5,9 @@ import React from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {Alert, FlatList} from 'react-native';
|
||||
|
||||
import {goToScreen} from '@actions/navigation';
|
||||
import {THREAD} from '@constants/screen';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import ThreadList from './thread_list';
|
||||
|
||||
import type {ActionResult} from '@mm-redux/types/actions';
|
||||
import type {Post} from '@mm-redux/types/posts';
|
||||
import type {Team} from '@mm-redux/types/teams';
|
||||
import type {Theme} from '@mm-redux/types/theme';
|
||||
import type {ThreadsState, UserThread} from '@mm-redux/types/threads';
|
||||
@@ -21,12 +16,10 @@ import type {$ID} from '@mm-redux/types/utilities';
|
||||
|
||||
type Props = {
|
||||
actions: {
|
||||
getPostThread: (postId: string) => void;
|
||||
getThreads: (userId: $ID<UserProfile>, teamId: $ID<Team>, before?: $ID<UserThread>, after?: $ID<UserThread>, perPage?: number, deleted?: boolean, unread?: boolean) => Promise<ActionResult>;
|
||||
handleViewingGlobalThreadsAll: () => void;
|
||||
handleViewingGlobalThreadsUnreads: () => void;
|
||||
markAllThreadsInTeamRead: (userId: $ID<UserProfile>, teamId: $ID<Team>) => void;
|
||||
selectPost: (postId: string) => void;
|
||||
};
|
||||
allThreadIds: $ID<UserThread>[];
|
||||
intl: typeof intlShape;
|
||||
@@ -45,7 +38,6 @@ function GlobalThreadsList({actions, allThreadIds, intl, teamId, theme, threadCo
|
||||
const listRef = React.useRef<FlatList>(null);
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(true);
|
||||
const [isRefreshing, setIsRefreshing] = React.useState<boolean>(false);
|
||||
|
||||
const scrollToTop = () => {
|
||||
listRef.current?.scrollToOffset({offset: 0});
|
||||
@@ -87,16 +79,6 @@ function GlobalThreadsList({actions, allThreadIds, intl, teamId, theme, threadCo
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = async () => {
|
||||
if (!isLoading) {
|
||||
if (!isRefreshing) {
|
||||
setIsRefreshing(true);
|
||||
}
|
||||
await loadThreads('', '', viewingUnreads);
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const markAllAsRead = () => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
@@ -126,32 +108,13 @@ function GlobalThreadsList({actions, allThreadIds, intl, teamId, theme, threadCo
|
||||
);
|
||||
};
|
||||
|
||||
const goToThread = React.useCallback((post: Post) => {
|
||||
actions.getPostThread(post.id);
|
||||
actions.selectPost(post.id);
|
||||
const passProps = {
|
||||
channelId: post.channel_id,
|
||||
rootId: post.id,
|
||||
};
|
||||
goToScreen(THREAD, '', passProps);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
EventEmitter.on('goToThread', goToThread);
|
||||
return () => {
|
||||
EventEmitter.off('goToThread', goToThread);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThreadList
|
||||
haveUnreads={haveUnreads}
|
||||
isLoading={isLoading}
|
||||
isRefreshing={isRefreshing}
|
||||
listRef={listRef}
|
||||
loadMoreThreads={loadMoreThreads}
|
||||
markAllAsRead={markAllAsRead}
|
||||
onRefresh={onRefresh}
|
||||
testID={'global_threads'}
|
||||
theme={theme}
|
||||
threadIds={ids}
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {getPostThread} from '@actions/views/post';
|
||||
import {handleViewingGlobalThreadsAll, handleViewingGlobalThreadsUnreads} from '@actions/views/threads';
|
||||
import {selectPost} from '@mm-redux/actions/posts';
|
||||
import {getThreads, markAllThreadsInTeamRead} from '@mm-redux/actions/threads';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/common';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
@@ -34,12 +32,10 @@ function mapStateToProps(state: GlobalState) {
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getPostThread,
|
||||
getThreads,
|
||||
handleViewingGlobalThreadsAll,
|
||||
handleViewingGlobalThreadsUnreads,
|
||||
markAllThreadsInTeamRead,
|
||||
selectPost,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
exports[`Global Thread Item Should render thread item with unread messages dot 1`] = `
|
||||
<TouchableHighlight
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
testID="thread_item.post1.item"
|
||||
underlayColor="rgba(28,88,217,0.08)"
|
||||
@@ -186,7 +185,6 @@ exports[`Global Thread Item Should render thread item with unread messages dot 1
|
||||
|
||||
exports[`Global Thread Item Should show unread mentions count 1`] = `
|
||||
<TouchableHighlight
|
||||
onLongPress={[Function]}
|
||||
onPress={[Function]}
|
||||
testID="thread_item.post1.item"
|
||||
underlayColor="rgba(28,88,217,0.08)"
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {getPost} from '@actions/views/post';
|
||||
import {getPost, getPostThread} from '@actions/views/post';
|
||||
import {selectPost} from '@mm-redux/actions/posts';
|
||||
import {getChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getPost as getPostSelector} from '@mm-redux/selectors/entities/posts';
|
||||
import {getThread} from '@mm-redux/selectors/entities/threads';
|
||||
@@ -29,6 +30,8 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
getPost,
|
||||
getPostThread,
|
||||
selectPost,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@ import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
import {Text} from 'react-native';
|
||||
|
||||
import * as navigationActions from '@actions/navigation';
|
||||
import {THREAD} from '@constants/screen';
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {Channel} from '@mm-redux/types/channels';
|
||||
import {Post} from '@mm-redux/types/posts';
|
||||
import {UserThread} from '@mm-redux/types/threads';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {intl} from '@test/intl-test-helper';
|
||||
|
||||
import {ThreadItem} from './thread_item';
|
||||
@@ -98,7 +99,7 @@ describe('Global Thread Item', () => {
|
||||
});
|
||||
|
||||
test('Should goto threads when pressed on thread item', () => {
|
||||
EventEmitter.emit = jest.fn();
|
||||
const goToScreen = jest.spyOn(navigationActions, 'goToScreen');
|
||||
const wrapper = shallow(
|
||||
<ThreadItem
|
||||
{...baseProps}
|
||||
@@ -107,6 +108,6 @@ describe('Global Thread Item', () => {
|
||||
const threadItem = wrapper.find({testID: `${testIDPrefix}.item`});
|
||||
expect(threadItem.exists()).toBeTruthy();
|
||||
threadItem.simulate('press');
|
||||
expect(EventEmitter.emit).toHaveBeenCalledWith('goToThread', expect.anything());
|
||||
expect(goToScreen).toHaveBeenCalledWith(THREAD, expect.anything(), expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,28 +3,29 @@
|
||||
|
||||
import React from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {Keyboard, Text, TouchableHighlight, View} from 'react-native';
|
||||
import {View, Text, TouchableHighlight} from 'react-native';
|
||||
|
||||
import {showModalOverCurrentContext} from '@actions/navigation';
|
||||
import {goToScreen} from '@actions/navigation';
|
||||
import FriendlyDate from '@components/friendly_date';
|
||||
import RemoveMarkdown from '@components/remove_markdown';
|
||||
import {GLOBAL_THREADS} from '@constants/screen';
|
||||
import {GLOBAL_THREADS, THREAD} from '@constants/screen';
|
||||
import {Posts, Preferences} from '@mm-redux/constants';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {Channel} from '@mm-redux/types/channels';
|
||||
import {Post} from '@mm-redux/types/posts';
|
||||
import {UserThread} from '@mm-redux/types/threads';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import ThreadFooter from '../thread_footer';
|
||||
|
||||
import type {Channel} from '@mm-redux/types/channels';
|
||||
import type {Post} from '@mm-redux/types/posts';
|
||||
import type {Theme} from '@mm-redux/types/theme';
|
||||
import type {UserThread} from '@mm-redux/types/threads';
|
||||
import type {UserProfile} from '@mm-redux/types/users';
|
||||
|
||||
export type DispatchProps = {
|
||||
actions: {
|
||||
getPost: (postId: string) => void;
|
||||
getPostThread: (postId: string) => void;
|
||||
selectPost: (postId: string) => void;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -65,18 +66,13 @@ function ThreadItem({actions, channel, intl, post, threadId, testID, theme, thre
|
||||
const threadStarterName = displayUsername(threadStarter, Preferences.DISPLAY_PREFER_FULL_NAME);
|
||||
|
||||
const showThread = () => {
|
||||
EventEmitter.emit('goToThread', postItem);
|
||||
};
|
||||
|
||||
const showThreadOptions = () => {
|
||||
const screen = 'GlobalThreadOptions';
|
||||
actions.getPostThread(postItem.id);
|
||||
actions.selectPost(postItem.id);
|
||||
const passProps = {
|
||||
rootId: post.id,
|
||||
channelId: postItem.channel_id,
|
||||
rootId: postItem.id,
|
||||
};
|
||||
Keyboard.dismiss();
|
||||
requestAnimationFrame(() => {
|
||||
showModalOverCurrentContext(screen, passProps);
|
||||
});
|
||||
goToScreen(THREAD, '', passProps);
|
||||
};
|
||||
|
||||
const testIDPrefix = `${testID}.${postItem?.id}`;
|
||||
@@ -138,7 +134,6 @@ function ThreadItem({actions, channel, intl, post, threadId, testID, theme, thre
|
||||
return (
|
||||
<TouchableHighlight
|
||||
underlayColor={changeOpacity(theme.buttonBg, 0.08)}
|
||||
onLongPress={showThreadOptions}
|
||||
onPress={showThread}
|
||||
testID={`${testIDPrefix}.item`}
|
||||
>
|
||||
|
||||
@@ -86,98 +86,62 @@ exports[`Global Thread List Should render threads with functional tabs & mark al
|
||||
viewUnreadThreads={[MockFunction]}
|
||||
viewingUnreads={true}
|
||||
/>
|
||||
<PostListRefreshControl
|
||||
enabled={true}
|
||||
isInverted={false}
|
||||
onRefresh={[MockFunction]}
|
||||
refreshing={false}
|
||||
theme={
|
||||
<FlatList
|
||||
ListEmptyComponent={
|
||||
<EmptyState
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
isUnreads={true}
|
||||
/>
|
||||
}
|
||||
ListFooterComponent={null}
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
"flexGrow": 1,
|
||||
}
|
||||
}
|
||||
>
|
||||
<FlatList
|
||||
ListEmptyComponent={
|
||||
<EmptyState
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
isUnreads={true}
|
||||
/>
|
||||
data={
|
||||
Array [
|
||||
"thread1",
|
||||
]
|
||||
}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
numColumns={1}
|
||||
onEndReached={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
scrollIndicatorInsets={
|
||||
Object {
|
||||
"right": 1,
|
||||
}
|
||||
ListFooterComponent={null}
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
}
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
"thread1",
|
||||
]
|
||||
}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
onEndReached={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
onScroll={[Function]}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
scrollIndicatorInsets={
|
||||
Object {
|
||||
"right": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</PostListRefreshControl>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
`;
|
||||
|
||||
@@ -25,11 +25,9 @@ describe('Global Thread List', () => {
|
||||
haveUnreads: true,
|
||||
intl,
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
listRef: React.useRef<FlatList>(null),
|
||||
loadMoreThreads: jest.fn(),
|
||||
markAllAsRead,
|
||||
onRefresh: jest.fn(),
|
||||
testID,
|
||||
theme: Preferences.THEMES.denim,
|
||||
threadIds: ['thread1'],
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {FlatList, NativeSyntheticEvent, NativeScrollEvent, Platform, View} from 'react-native';
|
||||
import {FlatList, Platform, View} from 'react-native';
|
||||
|
||||
import EmptyState from '@components/global_threads/empty_state';
|
||||
import ThreadItem from '@components/global_threads/thread_item';
|
||||
import Loading from '@components/loading';
|
||||
import {INITIAL_BATCH_TO_RENDER} from '@components/post_list/post_list_config';
|
||||
import CustomRefreshControl from '@components/post_list/post_list_refresh_control';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
@@ -22,11 +21,9 @@ export type Props = {
|
||||
haveUnreads: boolean;
|
||||
intl: typeof intlShape;
|
||||
isLoading: boolean;
|
||||
isRefreshing: boolean;
|
||||
loadMoreThreads: () => Promise<void>;
|
||||
listRef: React.RefObject<FlatList>;
|
||||
markAllAsRead: () => void;
|
||||
onRefresh: () => void;
|
||||
testID: string;
|
||||
theme: Theme;
|
||||
threadIds: $ID<UserThread>[];
|
||||
@@ -35,11 +32,9 @@ export type Props = {
|
||||
viewingUnreads: boolean;
|
||||
};
|
||||
|
||||
function ThreadList({haveUnreads, intl, isLoading, isRefreshing, loadMoreThreads, listRef, markAllAsRead, onRefresh, testID, theme, threadIds, viewAllThreads, viewUnreadThreads, viewingUnreads}: Props) {
|
||||
function ThreadList({haveUnreads, intl, isLoading, loadMoreThreads, listRef, markAllAsRead, testID, theme, threadIds, viewAllThreads, viewUnreadThreads, viewingUnreads}: Props) {
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const [offsetY, setOffsetY] = React.useState(0);
|
||||
|
||||
const handleEndReached = React.useCallback(() => {
|
||||
loadMoreThreads();
|
||||
}, [loadMoreThreads, viewingUnreads]);
|
||||
@@ -56,17 +51,6 @@ function ThreadList({haveUnreads, intl, isLoading, isRefreshing, loadMoreThreads
|
||||
);
|
||||
}, [theme]);
|
||||
|
||||
const onScroll = React.useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (Platform.OS === 'android') {
|
||||
const {y} = event.nativeEvent.contentOffset;
|
||||
if (y === 0) {
|
||||
setOffsetY(y);
|
||||
} else if (offsetY === 0 && y !== 0) {
|
||||
setOffsetY(y);
|
||||
}
|
||||
}
|
||||
}, [offsetY]);
|
||||
|
||||
const renderHeader = () => {
|
||||
if (!viewingUnreads && !threadIds.length) {
|
||||
return null;
|
||||
@@ -112,30 +96,21 @@ function ThreadList({haveUnreads, intl, isLoading, isRefreshing, loadMoreThreads
|
||||
return (
|
||||
<View style={style.container}>
|
||||
{renderHeader()}
|
||||
<CustomRefreshControl
|
||||
enabled={offsetY === 0}
|
||||
isInverted={false}
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={onRefresh}
|
||||
theme={theme}
|
||||
>
|
||||
<FlatList
|
||||
contentContainerStyle={style.messagesContainer}
|
||||
data={threadIds}
|
||||
keyExtractor={keyExtractor}
|
||||
ListEmptyComponent={renderEmptyList()}
|
||||
ListFooterComponent={renderFooter()}
|
||||
onEndReached={handleEndReached}
|
||||
onEndReachedThreshold={2}
|
||||
onScroll={onScroll}
|
||||
ref={listRef}
|
||||
renderItem={renderPost}
|
||||
initialNumToRender={INITIAL_BATCH_TO_RENDER}
|
||||
maxToRenderPerBatch={Platform.select({android: 5})}
|
||||
removeClippedSubviews={true}
|
||||
scrollIndicatorInsets={style.listScrollIndicator}
|
||||
/>
|
||||
</CustomRefreshControl>
|
||||
<FlatList
|
||||
contentContainerStyle={style.messagesContainer}
|
||||
data={threadIds}
|
||||
keyExtractor={keyExtractor}
|
||||
ListEmptyComponent={renderEmptyList()}
|
||||
ListFooterComponent={renderFooter()}
|
||||
onEndReached={handleEndReached}
|
||||
onEndReachedThreshold={2}
|
||||
ref={listRef}
|
||||
renderItem={renderPost}
|
||||
initialNumToRender={INITIAL_BATCH_TO_RENDER}
|
||||
maxToRenderPerBatch={Platform.select({android: 5})}
|
||||
removeClippedSubviews={true}
|
||||
scrollIndicatorInsets={style.listScrollIndicator}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
|
||||
import {PureComponent} from 'react';
|
||||
import {Dimensions, EmitterSubscription} from 'react-native';
|
||||
|
||||
@@ -78,6 +78,7 @@ export default class MarkdownCodeBlock extends React.PureComponent {
|
||||
const actionText = formatMessage({id: 'mobile.markdown.code.copy_code', defaultMessage: 'Copy Code'});
|
||||
BottomSheet.showBottomSheetWithOptions({
|
||||
options: [actionText, cancelText],
|
||||
cancelButtonIndex: 1,
|
||||
}, (value) => {
|
||||
if (value !== 1) {
|
||||
this.handleCopyCode();
|
||||
|
||||
@@ -151,6 +151,7 @@ export default class MarkdownImage extends ImageViewPort {
|
||||
const actionText = formatMessage({id: 'mobile.markdown.link.copy_url', defaultMessage: 'Copy URL'});
|
||||
BottomSheet.showBottomSheetWithOptions({
|
||||
options: [actionText, cancelText],
|
||||
cancelButtonIndex: 1,
|
||||
}, (value) => {
|
||||
if (value !== 1) {
|
||||
this.handleLinkCopy();
|
||||
|
||||
@@ -137,6 +137,7 @@ export default class MarkdownLink extends PureComponent {
|
||||
const actionText = formatMessage({id: 'mobile.markdown.link.copy_url', defaultMessage: 'Copy URL'});
|
||||
BottomSheet.showBottomSheetWithOptions({
|
||||
options: [actionText, cancelText],
|
||||
cancelButtonIndex: 1,
|
||||
}, (value) => {
|
||||
if (value !== 1) {
|
||||
this.handleLinkCopy();
|
||||
|
||||
@@ -286,7 +286,6 @@ exports[`PostDraft Should render the DraftInput 1`] = `
|
||||
<View>
|
||||
<PasteInput
|
||||
accessible={true}
|
||||
allowFontScaling={true}
|
||||
autoCapitalize="sentences"
|
||||
autoCompleteType="off"
|
||||
blurOnSubmit={false}
|
||||
@@ -304,8 +303,6 @@ exports[`PostDraft Should render the DraftInput 1`] = `
|
||||
onEndEditing={[Function]}
|
||||
onFocus={[Function]}
|
||||
onPaste={[Function]}
|
||||
onPressIn={[Function]}
|
||||
onPressOut={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
@@ -419,11 +416,6 @@ exports[`PostDraft Should render the DraftInput 1`] = `
|
||||
testID="post_draft.quick_actions"
|
||||
>
|
||||
<View
|
||||
accessibilityState={
|
||||
Object {
|
||||
"disabled": false,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
@@ -452,11 +444,6 @@ exports[`PostDraft Should render the DraftInput 1`] = `
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
accessibilityState={
|
||||
Object {
|
||||
"disabled": false,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
@@ -485,11 +472,6 @@ exports[`PostDraft Should render the DraftInput 1`] = `
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
accessibilityState={
|
||||
Object {
|
||||
"disabled": false,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
@@ -513,16 +495,11 @@ exports[`PostDraft Should render the DraftInput 1`] = `
|
||||
>
|
||||
<Icon
|
||||
color="rgba(63,67,80,0.64)"
|
||||
name="file-text-outline"
|
||||
name="file-document-outline"
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
accessibilityState={
|
||||
Object {
|
||||
"disabled": false,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
@@ -551,11 +528,6 @@ exports[`PostDraft Should render the DraftInput 1`] = `
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
accessibilityState={
|
||||
Object {
|
||||
"disabled": false,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
|
||||
@@ -329,13 +329,12 @@ export default class DraftInput extends PureComponent {
|
||||
const notificationsToChannel = enableConfirmNotificationsToChannel && useChannelMentions;
|
||||
const notificationsToGroups = enableConfirmNotificationsToChannel && useGroupMentions;
|
||||
const toAllOrChannel = DraftUtils.textContainsAtAllAtChannel(value);
|
||||
const toHere = DraftUtils.textContainsAtHere(value);
|
||||
const groupMentions = (!toAllOrChannel && !toHere && notificationsToGroups) ? DraftUtils.groupsMentionedInText(groupsWithAllowReference, value) : [];
|
||||
const groupMentions = (!toAllOrChannel && notificationsToGroups) ? DraftUtils.groupsMentionedInText(groupsWithAllowReference, value) : [];
|
||||
|
||||
if (value.indexOf('/') === 0) {
|
||||
this.sendCommand(value);
|
||||
} else if (notificationsToChannel && membersCount > NOTIFY_ALL_MEMBERS && (toAllOrChannel || toHere)) {
|
||||
this.showSendToAllOrChannelOrHereAlert(membersCount, value, toHere && !toAllOrChannel);
|
||||
} else if (notificationsToChannel && membersCount > NOTIFY_ALL_MEMBERS && toAllOrChannel) {
|
||||
this.showSendToAllOrChannelAlert(membersCount, value);
|
||||
} else if (groupMentions.length > 0) {
|
||||
const {groupMentionsSet, memberNotifyCount, channelTimezoneCount} = DraftUtils.mapGroupMentions(channelMemberCountsByGroup, groupMentions);
|
||||
if (memberNotifyCount > 0) {
|
||||
@@ -365,11 +364,11 @@ export default class DraftInput extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
showSendToAllOrChannelOrHereAlert = (membersCount, msg, atHere) => {
|
||||
showSendToAllOrChannelAlert = (membersCount, msg) => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {channelTimezoneCount} = this.state;
|
||||
const {isTimezoneEnabled} = this.props;
|
||||
const notifyAllMessage = DraftUtils.buildChannelWideMentionMessage(formatMessage, membersCount, isTimezoneEnabled, channelTimezoneCount, atHere);
|
||||
const notifyAllMessage = DraftUtils.buildChannelWideMentionMessage(formatMessage, membersCount, isTimezoneEnabled, channelTimezoneCount);
|
||||
const cancel = () => {
|
||||
this.setInputValue(msg);
|
||||
this.setState({sendingMessage: false});
|
||||
|
||||
@@ -110,31 +110,6 @@ describe('DraftInput', () => {
|
||||
expect(Alert.alert).toHaveBeenCalledWith('Confirm sending notifications to entire channel', expect.anything(), expect.anything());
|
||||
});
|
||||
|
||||
test('should send an alert when sending a message with a @here mention', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<DraftInput
|
||||
{...baseProps}
|
||||
ref={ref}
|
||||
/>,
|
||||
);
|
||||
const message = '@here';
|
||||
const instance = wrapper.instance();
|
||||
expect(instance.input).toEqual({current: null});
|
||||
instance.input = {
|
||||
current: {
|
||||
getValue: () => message,
|
||||
setValue: jest.fn(),
|
||||
changeDraft: jest.fn(),
|
||||
resetTextInput: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
instance.handleSendMessage();
|
||||
jest.runOnlyPendingTimers();
|
||||
expect(Alert.alert).toBeCalled();
|
||||
expect(Alert.alert).toHaveBeenCalledWith('Confirm sending notifications to entire channel', expect.anything(), expect.anything());
|
||||
});
|
||||
|
||||
test('should send an alert when sending a message with a group mention with group with count more than NOTIFY_ALL', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<DraftInput
|
||||
|
||||
@@ -41,9 +41,6 @@ export default class PostDraft extends PureComponent {
|
||||
|
||||
componentWillUnmount() {
|
||||
EventEmitter.off(UPDATE_NATIVE_SCROLLVIEW, this.updateNativeScrollView);
|
||||
if (this.resetScrollView) {
|
||||
cancelAnimationFrame(this.resetScrollView);
|
||||
}
|
||||
}
|
||||
|
||||
handleInputQuickAction = (value) => {
|
||||
@@ -54,10 +51,9 @@ export default class PostDraft extends PureComponent {
|
||||
|
||||
updateNativeScrollView = (scrollViewNativeID) => {
|
||||
if (this.keyboardTracker?.current && this.props.scrollViewNativeID === scrollViewNativeID) {
|
||||
this.resetScrollView = requestAnimationFrame(() => {
|
||||
this.keyboardTracker.current?.resetScrollView(scrollViewNativeID);
|
||||
cancelAnimationFrame(this.resetScrollView);
|
||||
this.resetScrollView = null;
|
||||
const resetScrollView = requestAnimationFrame(() => {
|
||||
this.keyboardTracker.current.resetScrollView(scrollViewNativeID);
|
||||
cancelAnimationFrame(resetScrollView);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
exports[`PostInput should match, full snapshot 1`] = `
|
||||
<ForwardRef
|
||||
allowFontScaling={true}
|
||||
autoCompleteType="off"
|
||||
blurOnSubmit={false}
|
||||
disableCopyPaste={false}
|
||||
@@ -13,8 +12,6 @@ exports[`PostInput should match, full snapshot 1`] = `
|
||||
onChangeText={[Function]}
|
||||
onEndEditing={[Function]}
|
||||
onPaste={[Function]}
|
||||
onPressIn={[Function]}
|
||||
onPressOut={[Function]}
|
||||
onSelectionChange={[Function]}
|
||||
placeholder="Write to Test Channel"
|
||||
placeholderTextColor="rgba(63,67,80,0.5)"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {PureComponent} from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
import {Alert, AppState, DeviceEventEmitter, findNodeHandle, Keyboard, NativeModules, Platform} from 'react-native';
|
||||
import {Alert, AppState, findNodeHandle, Keyboard, NativeModules, Platform} from 'react-native';
|
||||
|
||||
import {NavigationTypes} from '@constants';
|
||||
import DEVICE from '@constants/device';
|
||||
@@ -285,18 +285,6 @@ export default class PostInput extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
onPressIn = () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
DeviceEventEmitter.emit(NavigationTypes.DRAWER, 'locked-closed');
|
||||
}
|
||||
};
|
||||
|
||||
onPressOut = () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
DeviceEventEmitter.emit(NavigationTypes.DRAWER, 'unlocked');
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {testID, channelDisplayName, screenId, isLandscape, theme} = this.props;
|
||||
@@ -310,7 +298,6 @@ export default class PostInput extends PureComponent {
|
||||
|
||||
return (
|
||||
<PasteableTextInput
|
||||
allowFontScaling={true}
|
||||
testID={testID}
|
||||
ref={this.input}
|
||||
disableCopyPaste={this.state.disableCopyAndPaste}
|
||||
@@ -330,8 +317,6 @@ export default class PostInput extends PureComponent {
|
||||
autoCompleteType='off'
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
screenId={screenId}
|
||||
onPressIn={this.onPressIn}
|
||||
onPressOut={this.onPressOut}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
|
||||
exports[`CameraButton should match snapshot 1`] = `
|
||||
<View
|
||||
accessibilityState={
|
||||
Object {
|
||||
"disabled": false,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
|
||||
exports[`FileQuickAction should match snapshot 1`] = `
|
||||
<View
|
||||
accessibilityState={
|
||||
Object {
|
||||
"disabled": false,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
@@ -30,7 +25,7 @@ exports[`FileQuickAction should match snapshot 1`] = `
|
||||
>
|
||||
<Icon
|
||||
color="rgba(63,67,80,0.64)"
|
||||
name="file-text-outline"
|
||||
name="file-document-outline"
|
||||
size={24}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -83,7 +83,7 @@ const FileQuickAction = ({disabled, fileCount = 0, intl, maxFileCount, onUploadF
|
||||
>
|
||||
<CompassIcon
|
||||
color={color}
|
||||
name='file-text-outline'
|
||||
name='file-document-outline'
|
||||
size={ICON_SIZE}
|
||||
/>
|
||||
</TouchableWithFeedback>
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
|
||||
exports[`ImageQuickAction should match snapshot 1`] = `
|
||||
<View
|
||||
accessibilityState={
|
||||
Object {
|
||||
"disabled": false,
|
||||
}
|
||||
}
|
||||
accessible={true}
|
||||
collapsable={false}
|
||||
focusable={true}
|
||||
|
||||
@@ -213,16 +213,10 @@ export default class Uploads extends PureComponent {
|
||||
|
||||
let exceed = false;
|
||||
|
||||
const uploadFiles = [];
|
||||
const totalFiles = files.length;
|
||||
let i = 0;
|
||||
while (i < totalFiles) {
|
||||
const file = files[i];
|
||||
if (file.error) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!file.fileSize | !file.fileName) {
|
||||
const path = (file.path || file.uri).replace('file://', '');
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
@@ -235,7 +229,6 @@ export default class Uploads extends PureComponent {
|
||||
exceed = true;
|
||||
break;
|
||||
}
|
||||
uploadFiles.push(file);
|
||||
|
||||
i++;
|
||||
}
|
||||
@@ -243,7 +236,7 @@ export default class Uploads extends PureComponent {
|
||||
if (exceed) {
|
||||
this.handleFileSizeWarning();
|
||||
} else {
|
||||
this.props.initUploadFiles(uploadFiles, this.props.rootId);
|
||||
this.props.initUploadFiles(files, this.props.rootId);
|
||||
this.hideError();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ exports[`MoreMessagesButton should match snapshot 1`] = `
|
||||
Object {
|
||||
"transform": Array [
|
||||
Object {
|
||||
"translateY": -550,
|
||||
"translateY": -438,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ import {shallowWithIntl} from '@test/intl-test-helper';
|
||||
import MoreMessagesButton, {
|
||||
MIN_INPUT,
|
||||
MAX_INPUT,
|
||||
BARS_FACTOR,
|
||||
INDICATOR_BAR_FACTOR,
|
||||
CANCEL_TIMER_DELAY,
|
||||
} from './more_messages_button';
|
||||
|
||||
@@ -296,19 +296,17 @@ describe('MoreMessagesButton', () => {
|
||||
expect(Animated.spring).not.toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should animate to MAX_INPUT - BARS_FACTOR if visible and indicator bar hides', () => {
|
||||
it('should animate to MAX_INPUT - INDICATOR_BAR_FACTOR if visible and indicator bar hides', () => {
|
||||
instance.buttonVisible = true;
|
||||
instance.onIndicatorBarVisible(false);
|
||||
expect(Animated.spring).toHaveBeenCalledWith(instance.top, {
|
||||
toValue: MAX_INPUT - BARS_FACTOR,
|
||||
toValue: MAX_INPUT - INDICATOR_BAR_FACTOR,
|
||||
useNativeDriver: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should animate to MAX_INPUT if visible and indicator becomes visible', () => {
|
||||
instance.buttonVisible = true;
|
||||
instance.joinCallBarVisible = true;
|
||||
instance.currentCallBarVisible = true;
|
||||
instance.onIndicatorBarVisible(true);
|
||||
expect(Animated.spring).toHaveBeenCalledWith(instance.top, {
|
||||
toValue: MAX_INPUT,
|
||||
@@ -416,15 +414,13 @@ describe('MoreMessagesButton', () => {
|
||||
instance.show();
|
||||
expect(instance.buttonVisible).toBe(true);
|
||||
expect(Animated.spring).toHaveBeenCalledWith(instance.top, {
|
||||
toValue: MAX_INPUT - BARS_FACTOR,
|
||||
toValue: MAX_INPUT - INDICATOR_BAR_FACTOR,
|
||||
useNativeDriver: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should account for the indicator bar heights when the indicator is visible', () => {
|
||||
it('should account for the indicator bar height when the indicator is visible', () => {
|
||||
instance.indicatorBarVisible = true;
|
||||
instance.joinCallBarVisible = true;
|
||||
instance.currentCallBarVisible = true;
|
||||
instance.buttonVisible = false;
|
||||
wrapper.setState({moreText: '1 new message'});
|
||||
wrapper.setProps({deepLinkURL: null, unreadCount: 1});
|
||||
@@ -461,15 +457,13 @@ describe('MoreMessagesButton', () => {
|
||||
instance.hide();
|
||||
expect(instance.buttonVisible).toBe(false);
|
||||
expect(Animated.spring).toHaveBeenCalledWith(instance.top, {
|
||||
toValue: MIN_INPUT + BARS_FACTOR,
|
||||
toValue: MIN_INPUT + INDICATOR_BAR_FACTOR,
|
||||
useNativeDriver: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should account for the indicator bars heights when the indicator is visible', () => {
|
||||
it('should account for the indicator bar height when the indicator is visible', () => {
|
||||
instance.indicatorBarVisible = true;
|
||||
instance.joinCallBarVisible = true;
|
||||
instance.currentCallBarVisible = true;
|
||||
instance.buttonVisible = true;
|
||||
|
||||
instance.hide();
|
||||
|
||||
@@ -7,7 +7,7 @@ import {ActivityIndicator, Animated, AppState, AppStateStatus, NativeEventSubscr
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import TouchableWithFeedback from '@components/touchable_with_feedback';
|
||||
import ViewTypes, {INDICATOR_BAR_HEIGHT, JOIN_CALL_BAR_HEIGHT, CURRENT_CALL_BAR_HEIGHT} from '@constants/view';
|
||||
import ViewTypes, {INDICATOR_BAR_HEIGHT} from '@constants/view';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {messageCount} from '@mm-redux/utils/post_list';
|
||||
import {t} from '@utils/i18n';
|
||||
@@ -17,22 +17,22 @@ import type {Theme} from '@mm-redux/types/theme';
|
||||
|
||||
const HIDDEN_TOP = -400;
|
||||
const SHOWN_TOP = 0;
|
||||
export const BARS_FACTOR = Math.abs((INDICATOR_BAR_HEIGHT + JOIN_CALL_BAR_HEIGHT + CURRENT_CALL_BAR_HEIGHT) / (HIDDEN_TOP - SHOWN_TOP));
|
||||
export const INDICATOR_BAR_FACTOR = Math.abs(INDICATOR_BAR_HEIGHT / (HIDDEN_TOP - SHOWN_TOP));
|
||||
export const MIN_INPUT = 0;
|
||||
export const MAX_INPUT = 1;
|
||||
|
||||
const TOP_INTERPOL_CONFIG: Animated.InterpolationConfigType = {
|
||||
inputRange: [
|
||||
MIN_INPUT,
|
||||
MIN_INPUT + BARS_FACTOR,
|
||||
MAX_INPUT - BARS_FACTOR,
|
||||
MIN_INPUT + INDICATOR_BAR_FACTOR,
|
||||
MAX_INPUT - INDICATOR_BAR_FACTOR,
|
||||
MAX_INPUT,
|
||||
],
|
||||
outputRange: [
|
||||
HIDDEN_TOP - (INDICATOR_BAR_HEIGHT + JOIN_CALL_BAR_HEIGHT + CURRENT_CALL_BAR_HEIGHT),
|
||||
HIDDEN_TOP - INDICATOR_BAR_HEIGHT,
|
||||
HIDDEN_TOP,
|
||||
SHOWN_TOP,
|
||||
SHOWN_TOP + INDICATOR_BAR_HEIGHT + JOIN_CALL_BAR_HEIGHT + CURRENT_CALL_BAR_HEIGHT,
|
||||
SHOWN_TOP + INDICATOR_BAR_HEIGHT,
|
||||
],
|
||||
extrapolate: 'clamp',
|
||||
};
|
||||
@@ -71,8 +71,6 @@ export default class MoreMessageButton extends React.PureComponent<MoreMessagesB
|
||||
disableViewableItems = false;
|
||||
endIndex: number | null = null;
|
||||
indicatorBarVisible = false;
|
||||
joinCallBarVisible = false;
|
||||
currentCallBarVisible = false;
|
||||
pressed = false;
|
||||
removeViewableItemsListener: undefined | (() => void) = undefined;
|
||||
removeScrollEndIndexListener: undefined | (() => void) = undefined;
|
||||
@@ -83,8 +81,6 @@ export default class MoreMessageButton extends React.PureComponent<MoreMessagesB
|
||||
componentDidMount() {
|
||||
this.appStateListener = AppState.addEventListener('change', this.onAppStateChange);
|
||||
EventEmitter.on(ViewTypes.INDICATOR_BAR_VISIBLE, this.onIndicatorBarVisible);
|
||||
EventEmitter.on(ViewTypes.JOIN_CALL_BAR_VISIBLE, this.onJoinCallBarVisible);
|
||||
EventEmitter.on(ViewTypes.CURRENT_CALL_BAR_VISIBLE, this.onCurrentCallBarVisible);
|
||||
this.removeViewableItemsListener = this.props.registerViewableItemsListener(this.onViewableItemsChanged);
|
||||
this.removeScrollEndIndexListener = this.props.registerScrollEndIndexListener(this.onScrollEndIndex);
|
||||
}
|
||||
@@ -92,8 +88,6 @@ export default class MoreMessageButton extends React.PureComponent<MoreMessagesB
|
||||
componentWillUnmount() {
|
||||
this.appStateListener?.remove();
|
||||
EventEmitter.off(ViewTypes.INDICATOR_BAR_VISIBLE, this.onIndicatorBarVisible);
|
||||
EventEmitter.off(ViewTypes.JOIN_CALL_BAR_VISIBLE, this.onJoinCallBarVisible);
|
||||
EventEmitter.off(ViewTypes.CURRENT_CALL_BAR_VISIBLE, this.onCurrentCallBarVisible);
|
||||
if (this.removeViewableItemsListener) {
|
||||
this.removeViewableItemsListener();
|
||||
}
|
||||
@@ -150,22 +144,8 @@ export default class MoreMessageButton extends React.PureComponent<MoreMessagesB
|
||||
|
||||
onIndicatorBarVisible = (indicatorVisible: boolean) => {
|
||||
this.indicatorBarVisible = indicatorVisible;
|
||||
this.animateButton();
|
||||
}
|
||||
|
||||
onCurrentCallBarVisible = (currentCallVisible: boolean) => {
|
||||
this.currentCallBarVisible = currentCallVisible;
|
||||
this.animateButton();
|
||||
}
|
||||
|
||||
onJoinCallBarVisible = (joinCallVisible: boolean) => {
|
||||
this.joinCallBarVisible = joinCallVisible;
|
||||
this.animateButton();
|
||||
}
|
||||
|
||||
animateButton = () => {
|
||||
if (this.buttonVisible) {
|
||||
const toValue = MAX_INPUT - this.getBarsFactor();
|
||||
const toValue = this.indicatorBarVisible ? MAX_INPUT : MAX_INPUT - INDICATOR_BAR_FACTOR;
|
||||
Animated.spring(this.top, {
|
||||
toValue,
|
||||
useNativeDriver: true,
|
||||
@@ -189,22 +169,18 @@ export default class MoreMessageButton extends React.PureComponent<MoreMessagesB
|
||||
show = () => {
|
||||
if (!this.buttonVisible && this.state.moreText && !this.props.deepLinkURL && !this.canceled && this.props.unreadCount > 0) {
|
||||
this.buttonVisible = true;
|
||||
this.animateButton();
|
||||
const toValue = this.indicatorBarVisible ? MAX_INPUT : MAX_INPUT - INDICATOR_BAR_FACTOR;
|
||||
Animated.spring(this.top, {
|
||||
toValue,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
getBarsFactor = () => {
|
||||
return Math.abs((
|
||||
(this.indicatorBarVisible ? 0 : INDICATOR_BAR_HEIGHT) +
|
||||
(this.joinCallBarVisible ? 0 : JOIN_CALL_BAR_HEIGHT) +
|
||||
(this.currentCallBarVisible ? 0 : CURRENT_CALL_BAR_HEIGHT)
|
||||
) / (HIDDEN_TOP - SHOWN_TOP));
|
||||
}
|
||||
|
||||
hide = () => {
|
||||
if (this.buttonVisible) {
|
||||
this.buttonVisible = false;
|
||||
const toValue = MIN_INPUT + this.getBarsFactor();
|
||||
const toValue = this.indicatorBarVisible ? MIN_INPUT : MIN_INPUT + INDICATOR_BAR_FACTOR;
|
||||
Animated.spring(this.top, {
|
||||
toValue,
|
||||
useNativeDriver: true,
|
||||
|
||||
@@ -200,6 +200,4 @@ const DocumentFile = forwardRef<DocumentFileRef, DocumentFileProps>(({background
|
||||
);
|
||||
});
|
||||
|
||||
DocumentFile.displayName = 'DocumentFile';
|
||||
|
||||
export default injectIntl(DocumentFile, {withRef: true});
|
||||
|
||||
@@ -25,21 +25,21 @@ const BLUE_ICON = '#338AFF';
|
||||
const RED_ICON = '#ED522A';
|
||||
const GREEN_ICON = '#1CA660';
|
||||
const GRAY_ICON = '#999999';
|
||||
const FAILED_ICON_NAME_AND_COLOR = ['file-image-broken-outline-large', GRAY_ICON];
|
||||
const FAILED_ICON_NAME_AND_COLOR = ['jumbo-attachment-image-broken', GRAY_ICON];
|
||||
const ICON_NAME_AND_COLOR_FROM_FILE_TYPE: Record<string, string[]> = {
|
||||
audio: ['file-audio-outline-large', BLUE_ICON],
|
||||
code: ['file-code-outline-large', BLUE_ICON],
|
||||
image: ['file-image-outline-large', BLUE_ICON],
|
||||
audio: ['jumbo-attachment-audio', BLUE_ICON],
|
||||
code: ['jumbo-attachment-code', BLUE_ICON],
|
||||
image: ['jumbo-attachment-image', BLUE_ICON],
|
||||
smallImage: ['image-outline', BLUE_ICON],
|
||||
other: ['file-generic-outline-large', BLUE_ICON],
|
||||
patch: ['file-patch-outline-large', BLUE_ICON],
|
||||
pdf: ['file-pdf-outline-large', RED_ICON],
|
||||
presentation: ['file-powerpoint-outline-large', RED_ICON],
|
||||
spreadsheet: ['file-excel-outline-large', GREEN_ICON],
|
||||
text: ['file-text-outline-large', GRAY_ICON],
|
||||
video: ['file-video-outline-large', BLUE_ICON],
|
||||
word: ['file-word-outline-large', BLUE_ICON],
|
||||
zip: ['file-zip-outline-large', BLUE_ICON],
|
||||
other: ['jumbo-attachment-generic', BLUE_ICON],
|
||||
patch: ['jumbo-attachment-patch', BLUE_ICON],
|
||||
pdf: ['jumbo-attachment-pdf', RED_ICON],
|
||||
presentation: ['jumbo-attachment-powerpoint', RED_ICON],
|
||||
spreadsheet: ['jumbo-attachment-excel', GREEN_ICON],
|
||||
text: ['jumbo-attachment-text', GRAY_ICON],
|
||||
video: ['jumbo-attachment-video', BLUE_ICON],
|
||||
word: ['jumbo-attachment-word', BLUE_ICON],
|
||||
zip: ['jumbo-attachment-zip', BLUE_ICON],
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useMemo, useState} from 'react';
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {DeviceEventEmitter, StyleProp, StyleSheet, View, ViewStyle} from 'react-native';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
@@ -46,10 +46,10 @@ const Files = ({canDownloadFiles, failed, files, isReplyPost, postId, theme}: Fi
|
||||
const [inViewPort, setInViewPort] = useState(false);
|
||||
const permanentSidebar = usePermanentSidebar();
|
||||
const isSplitView = useSplitView();
|
||||
const {imageAttachments, nonImageAttachments} = useMemo(() => {
|
||||
const images: FileInfo[] = [];
|
||||
const nonImages: FileInfo[] = [];
|
||||
const imageAttachments = useRef<FileInfo[]>([]).current;
|
||||
const nonImageAttachments = useRef<FileInfo[]>([]).current;
|
||||
|
||||
if (!imageAttachments.length && !nonImageAttachments.length) {
|
||||
files.reduce((info, file) => {
|
||||
if (isImage(file)) {
|
||||
let uri;
|
||||
@@ -58,17 +58,15 @@ const Files = ({canDownloadFiles, failed, files, isReplyPost, postId, theme}: Fi
|
||||
} else {
|
||||
uri = isGif(file) ? Client4.getFileUrl(file.id, 0) : Client4.getFilePreviewUrl(file.id, 0);
|
||||
}
|
||||
info.images.push({...file, uri});
|
||||
info.imageAttachments.push({...file, uri});
|
||||
} else {
|
||||
info.nonImages.push(file);
|
||||
info.nonImageAttachments.push(file);
|
||||
}
|
||||
return info;
|
||||
}, {images, nonImages});
|
||||
}, {imageAttachments, nonImageAttachments});
|
||||
}
|
||||
|
||||
return {imageAttachments: images, nonImageAttachments: nonImages};
|
||||
}, [files]);
|
||||
|
||||
const filesForGallery = imageAttachments.concat(nonImageAttachments);
|
||||
const filesForGallery = useRef<FileInfo[]>(imageAttachments.concat(nonImageAttachments)).current;
|
||||
const attachmentIndex = (fileId: string) => {
|
||||
return filesForGallery.findIndex((file) => file.id === fileId) || 0;
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import {showPermalink} from '@actions/views/permalink';
|
||||
import {THREAD} from '@constants/screen';
|
||||
import {removePost} from '@mm-redux/actions/posts';
|
||||
import {getChannel} from '@mm-redux/selectors/entities/channels';
|
||||
import {getConfig, getFeatureFlagValue} from '@mm-redux/selectors/entities/general';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getPost, isRootPost} from '@mm-redux/selectors/entities/posts';
|
||||
import {getMyPreferences, getTeammateNameDisplaySetting, isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
@@ -49,7 +49,6 @@ function mapSateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
|
||||
const enablePostUsernameOverride = config.EnablePostUsernameOverride === 'true';
|
||||
const isConsecutivePost = post && previousPost && !author?.is_bot && !isRootPost(state, post.id) && areConsecutivePosts(post, previousPost);
|
||||
const callsFeatureEnabled = getFeatureFlagValue(state, 'CallsMobile') === 'true';
|
||||
let isFirstReply = true;
|
||||
let isLastReply = true;
|
||||
let canDelete = false;
|
||||
@@ -98,7 +97,6 @@ function mapSateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
teammateNameDisplay,
|
||||
thread,
|
||||
threadStarter: getUser(state, post.user_id),
|
||||
callsFeatureEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import {UserThread} from '@mm-redux/types/threads';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import {fromAutoResponder, isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from '@mm-redux/utils/post_utils';
|
||||
import CallMessage from '@mmproducts/calls/components/call_message';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
@@ -55,7 +54,6 @@ type PostProps = {
|
||||
theme: Theme;
|
||||
thread: UserThread;
|
||||
threadStarter: UserProfile;
|
||||
callsFeatureEnabled: boolean;
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
@@ -115,7 +113,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
const Post = ({
|
||||
canDelete, collapsedThreadsEnabled, enablePostUsernameOverride, highlight, highlightPinnedOrFlagged = true, intl, isConsecutivePost, isFirstReply, isFlagged, isLastReply,
|
||||
location, post, removePost, rootPostAuthor, shouldRenderReplyButton, skipFlaggedHeader, skipPinnedHeader, showAddReaction = true, showPermalink, style,
|
||||
teammateNameDisplay, testID, theme, thread, threadStarter, callsFeatureEnabled,
|
||||
teammateNameDisplay, testID, theme, thread, threadStarter,
|
||||
}: PostProps) => {
|
||||
const pressDetected = useRef(false);
|
||||
const styles = getStyleSheet(theme);
|
||||
@@ -241,13 +239,6 @@ const Post = ({
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else if (post.type === 'custom_calls' && callsFeatureEnabled) {
|
||||
body = (
|
||||
<CallMessage
|
||||
post={post}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<Body
|
||||
|
||||
@@ -9,7 +9,6 @@ import type {Theme} from '@mm-redux/types/theme';
|
||||
type Props = {
|
||||
children: ReactElement;
|
||||
enabled: boolean;
|
||||
isInverted?: boolean;
|
||||
onRefresh: () => void;
|
||||
refreshing: boolean;
|
||||
theme: Theme;
|
||||
@@ -18,13 +17,11 @@ type Props = {
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
containerInverse: {
|
||||
scaleY: -1,
|
||||
},
|
||||
});
|
||||
|
||||
const PostListRefreshControl = ({children, enabled, isInverted = true, onRefresh, refreshing, theme}: Props) => {
|
||||
const PostListRefreshControl = ({children, enabled, onRefresh, refreshing, theme}: Props) => {
|
||||
const props = {
|
||||
colors: [theme.onlineIndicator, theme.awayIndicator, theme.dndIndicator],
|
||||
onRefresh,
|
||||
@@ -37,7 +34,7 @@ const PostListRefreshControl = ({children, enabled, isInverted = true, onRefresh
|
||||
<RefreshControl
|
||||
{...props}
|
||||
enabled={enabled}
|
||||
style={[style.container, isInverted ? style.containerInverse : undefined]}
|
||||
style={style.container}
|
||||
>
|
||||
{children}
|
||||
</RefreshControl>
|
||||
@@ -48,7 +45,7 @@ const PostListRefreshControl = ({children, enabled, isInverted = true, onRefresh
|
||||
|
||||
return React.cloneElement(
|
||||
children,
|
||||
{refreshControl, inverted: isInverted},
|
||||
{refreshControl, inverted: true},
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ exports[`SearchBar should match snapshot 1`] = `
|
||||
}
|
||||
>
|
||||
<ForwardRef(Themed.SearchBar)
|
||||
allowFontScaling={true}
|
||||
autoCapitalize="auto-capitalize"
|
||||
autoCorrect={false}
|
||||
autoFocus={true}
|
||||
|
||||
@@ -323,7 +323,6 @@ export default class Search extends PureComponent {
|
||||
]}
|
||||
>
|
||||
<SearchBar
|
||||
allowFontScaling={true}
|
||||
testID={searchInputTestID}
|
||||
autoCapitalize={this.props.autoCapitalize}
|
||||
autoCorrect={false}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {DeviceEventEmitter, EventSubscription, Platform} from 'react-native';
|
||||
import React from 'react';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import NavigationTypes from '@constants/navigation';
|
||||
|
||||
import DrawerLayout from './drawer_layout';
|
||||
|
||||
export const DRAWER_INITIAL_OFFSET = 40;
|
||||
@@ -33,23 +30,11 @@ interface DrawerLayoutAdapterProps {
|
||||
const DrawerLayoutAdapter = (props: DrawerLayoutAdapterProps) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const horizontal = insets.left + insets.right;
|
||||
const [drawerLockMode, setDrawerLockMode] = useState(props.drawerLockMode || 'unlocked');
|
||||
|
||||
useEffect(() => {
|
||||
let listener: EventSubscription | undefined;
|
||||
if (Platform.OS === 'ios') {
|
||||
listener = DeviceEventEmitter.addListener(NavigationTypes.DRAWER, (value) => {
|
||||
setDrawerLockMode(value);
|
||||
});
|
||||
}
|
||||
|
||||
return () => listener?.remove();
|
||||
});
|
||||
|
||||
return (
|
||||
<DrawerLayout
|
||||
drawerBackgroundColor={props.drawerBackgroundColor}
|
||||
drawerLockMode={drawerLockMode}
|
||||
drawerLockMode={props.drawerLockMode}
|
||||
drawerPosition={props.drawerPosition}
|
||||
drawerWidth={props.drawerWidth - horizontal}
|
||||
isTablet={props.isTablet}
|
||||
|
||||
@@ -808,240 +808,6 @@ exports[`ChannelItem should match snapshot for no displayName 1`] = `null`;
|
||||
|
||||
exports[`ChannelItem should match snapshot for showUnreadForMsgs 1`] = `null`;
|
||||
|
||||
exports[`ChannelItem should match snapshot when there is a call and but calls are disabled 1`] = `
|
||||
<TouchableHighlight
|
||||
onPress={[Function]}
|
||||
underlayColor="rgba(40,66,123,0.5)"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 44,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"paddingLeft": 16,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item.channel_id"
|
||||
>
|
||||
<ChannelIcon
|
||||
channelId="channel_id"
|
||||
hasDraft={false}
|
||||
isActive={false}
|
||||
isArchived={false}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
size={24}
|
||||
statusStyle={
|
||||
Object {
|
||||
"backgroundColor": "#1e325c",
|
||||
"borderColor": "transparent",
|
||||
}
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
type="O"
|
||||
/>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignSelf": "center",
|
||||
"color": "rgba(255,255,255,0.6)",
|
||||
"fontFamily": "Open Sans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 24,
|
||||
"marginLeft": 13,
|
||||
"maxWidth": "80%",
|
||||
"paddingRight": 10,
|
||||
},
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"fontWeight": "500",
|
||||
"maxWidth": "70%",
|
||||
"opacity": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item.display_name"
|
||||
>
|
||||
display_name
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
`;
|
||||
|
||||
exports[`ChannelItem should match snapshot when there is a call and calls are enabled 1`] = `
|
||||
<TouchableHighlight
|
||||
onPress={[Function]}
|
||||
underlayColor="rgba(40,66,123,0.5)"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"height": 44,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"paddingLeft": 16,
|
||||
},
|
||||
undefined,
|
||||
]
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item.channel_id"
|
||||
>
|
||||
<ChannelIcon
|
||||
channelId="channel_id"
|
||||
hasDraft={false}
|
||||
isActive={false}
|
||||
isArchived={false}
|
||||
isInfo={false}
|
||||
isUnread={true}
|
||||
membersCount={1}
|
||||
size={24}
|
||||
statusStyle={
|
||||
Object {
|
||||
"backgroundColor": "#1e325c",
|
||||
"borderColor": "transparent",
|
||||
}
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item.channel_icon"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
type="O"
|
||||
/>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignSelf": "center",
|
||||
"color": "rgba(255,255,255,0.6)",
|
||||
"fontFamily": "Open Sans",
|
||||
"fontSize": 16,
|
||||
"lineHeight": 24,
|
||||
"marginLeft": 13,
|
||||
"maxWidth": "80%",
|
||||
"paddingRight": 10,
|
||||
},
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"fontWeight": "500",
|
||||
"maxWidth": "70%",
|
||||
"opacity": 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="main.sidebar.channels_list.list.channel_item.display_name"
|
||||
>
|
||||
display_name
|
||||
</Text>
|
||||
<CompassIcon
|
||||
name="phone-in-talk"
|
||||
size={16}
|
||||
style={
|
||||
Object {
|
||||
"color": "#ffffff",
|
||||
"flex": 1,
|
||||
"marginRight": 20,
|
||||
"textAlign": "right",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
`;
|
||||
|
||||
exports[`ChannelItem should match snapshot with custom status emoji 1`] = `
|
||||
<TouchableHighlight
|
||||
onPress={[Function]}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
|
||||
import Badge from '@components/badge';
|
||||
import ChannelIcon from '@components/channel_icon';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
@@ -42,8 +41,6 @@ export default class ChannelItem extends PureComponent {
|
||||
isSearchResult: PropTypes.bool,
|
||||
viewingGlobalThreads: PropTypes.bool,
|
||||
customStatusEnabled: PropTypes.bool.isRequired,
|
||||
channelHasCall: PropTypes.bool.isRequired,
|
||||
callsFeatureEnabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -217,13 +214,6 @@ export default class ChannelItem extends PureComponent {
|
||||
</Text>
|
||||
{customStatus}
|
||||
{badge}
|
||||
{this.props.callsFeatureEnabled && this.props.channelHasCall &&
|
||||
<CompassIcon
|
||||
name='phone-in-talk'
|
||||
size={16}
|
||||
style={style.hasCall}
|
||||
/>
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
@@ -298,11 +288,5 @@ export const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
muted: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
hasCall: {
|
||||
color: theme.sidebarText,
|
||||
flex: 1,
|
||||
textAlign: 'right',
|
||||
marginRight: 20,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -40,8 +40,6 @@ describe('ChannelItem', () => {
|
||||
isSearchResult: false,
|
||||
isBot: false,
|
||||
customStatusEnabled: true,
|
||||
channelHasCall: false,
|
||||
callsFeatureEnabled: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
@@ -52,34 +50,6 @@ describe('ChannelItem', () => {
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot when there is a call and calls are enabled', () => {
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
callsFeatureEnabled: true,
|
||||
channelHasCall: true,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelItem {...newProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot when there is a call and but calls are disabled', () => {
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
callsFeatureEnabled: false,
|
||||
channelHasCall: true,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<ChannelItem {...newProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with mentions and muted', () => {
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
|
||||
@@ -11,12 +11,10 @@ import {
|
||||
makeGetChannel,
|
||||
shouldHideDefaultChannel,
|
||||
} from '@mm-redux/selectors/entities/channels';
|
||||
import {getFeatureFlagValue} from '@mm-redux/selectors/entities/general';
|
||||
import {getTheme, getTeammateNameDisplaySetting, isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
|
||||
import {getMsgCountInChannel, getUserIdFromChannelName, isChannelMuted} from '@mm-redux/utils/channel_utils';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
import {getCalls} from '@mmproducts/calls/store/selectors/calls';
|
||||
import {isCustomStatusEnabled} from '@selectors/custom_status';
|
||||
import {getViewingGlobalThreads} from '@selectors/threads';
|
||||
import {getDraftForChannel} from '@selectors/views';
|
||||
@@ -33,7 +31,6 @@ function makeMapStateToProps() {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const channelDraft = getDraftForChannel(state, channel.id);
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
|
||||
const channelHasCall = Boolean(getCalls(state)[ownProps.channelId]);
|
||||
|
||||
let displayName = channel.display_name;
|
||||
let isGuest = false;
|
||||
@@ -75,7 +72,6 @@ function makeMapStateToProps() {
|
||||
if (member && member.notify_props) {
|
||||
showUnreadForMsgs = member.notify_props.mark_unread !== General.MENTION;
|
||||
}
|
||||
const callsFeatureEnabled = getFeatureFlagValue(state, 'CallsMobile') === 'true';
|
||||
|
||||
const viewingGlobalThreads = getViewingGlobalThreads(state);
|
||||
return {
|
||||
@@ -96,8 +92,6 @@ function makeMapStateToProps() {
|
||||
unreadMsgs,
|
||||
viewingGlobalThreads,
|
||||
customStatusEnabled: isCustomStatusEnabled(state),
|
||||
channelHasCall,
|
||||
callsFeatureEnabled,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ export default class ChannelsList extends PureComponent {
|
||||
<SearchBar
|
||||
testID={searchBarTestID}
|
||||
ref={this.setSearchBarRef}
|
||||
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Find channel'})}
|
||||
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Jump to...'})}
|
||||
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
backgroundColor='transparent'
|
||||
inputHeight={36}
|
||||
|
||||
@@ -83,7 +83,6 @@ function mapStateToProps(state) {
|
||||
showLegacySidebar,
|
||||
unreadsOnTop,
|
||||
currentChannelId,
|
||||
currentTeamId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ export default class List extends PureComponent {
|
||||
showLegacySidebar: PropTypes.bool.isRequired,
|
||||
unreadsOnTop: PropTypes.bool.isRequired,
|
||||
currentChannelId: PropTypes.string,
|
||||
currentTeamId: PropTypes.string,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -114,9 +113,10 @@ export default class List extends PureComponent {
|
||||
this.props.orderedChannelIds !== orderedChannelIds) {
|
||||
this.setSections(this.buildSections(this.props));
|
||||
}
|
||||
} else if ( // Rebuild sections only if categories or unreads have changed
|
||||
} else if (
|
||||
!isEqual(this.props.categories, categories) ||
|
||||
!isEqual(this.props.unreadChannelIds, unreadChannelIds)) {
|
||||
this.props.unreadChannelIds !== unreadChannelIds) {
|
||||
// Rebuild sections only if categories or unreads have changed
|
||||
this.setCategorySections(this.buildCategorySections());
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ export default class List extends PureComponent {
|
||||
const moreChannelsText = formatMessage({id: 'more_channels.title', defaultMessage: 'Browse for a Channel'});
|
||||
const newChannelText = formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create a new Channel'});
|
||||
const newDirectChannelText = formatMessage({id: 'mobile.more_dms.title', defaultMessage: 'Add a Conversation'});
|
||||
|
||||
const cancelText = formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'});
|
||||
const options = [];
|
||||
const actions = [];
|
||||
|
||||
@@ -229,14 +229,20 @@ export default class List extends PureComponent {
|
||||
|
||||
actions.push(this.goToDirectMessages);
|
||||
options.push({text: newDirectChannelText, icon: 'account-plus-outline'});
|
||||
options.push(cancelText);
|
||||
|
||||
const cancelButtonIndex = options.length - 1;
|
||||
|
||||
BottomSheet.showBottomSheetWithOptions({
|
||||
anchor: this.combinedActionsRef?.current ? findNodeHandle(this.combinedActionsRef.current) : null,
|
||||
options,
|
||||
title: 'Add Channels',
|
||||
subtitle: `To the ${category.display_name} category`,
|
||||
cancelButtonIndex,
|
||||
}, (value) => {
|
||||
actions[value]();
|
||||
if (value !== cancelButtonIndex) {
|
||||
actions[value]();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -420,21 +426,6 @@ export default class List extends PureComponent {
|
||||
}
|
||||
};
|
||||
|
||||
const categoryId = () => {
|
||||
switch (type) {
|
||||
case CategoryTypes.UNREADS:
|
||||
return CategoryTypes.UNREADS.toLowerCase();
|
||||
case CategoryTypes.FAVORITES:
|
||||
return CategoryTypes.FAVORITES.toLowerCase();
|
||||
case CategoryTypes.CHANNELS:
|
||||
return CategoryTypes.CHANNELS.toLowerCase();
|
||||
case CategoryTypes.DIRECT_MESSAGES:
|
||||
return CategoryTypes.DIRECT_MESSAGES.toLowerCase();
|
||||
default:
|
||||
return name.replace(/ /g, '_').toLowerCase();
|
||||
}
|
||||
};
|
||||
|
||||
const header = (
|
||||
<View style={styles.titleContainer}>
|
||||
{(type !== CategoryTypes.UNREADS && data.length > 0) &&
|
||||
@@ -450,7 +441,7 @@ export default class List extends PureComponent {
|
||||
<View style={styles.separatorContainer}>
|
||||
<Text> </Text>
|
||||
</View>
|
||||
{action && this.renderSectionAction(styles, action, anchor, categoryId())}
|
||||
{action && this.renderSectionAction(styles, action, anchor, id)}
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -481,9 +472,6 @@ export default class List extends PureComponent {
|
||||
// Add the rest
|
||||
if (this.props.categories) {
|
||||
this.props.categories.reduce((prev, cat) => {
|
||||
if (cat.type === 'favorites' && !cat.channel_ids.length) {
|
||||
return prev;
|
||||
}
|
||||
prev.push({
|
||||
name: cat.display_name,
|
||||
action: cat.type === 'direct_messages' ? this.goToDirectMessages : () => this.showCreateChannelOptions(cat),
|
||||
@@ -548,7 +536,7 @@ export default class List extends PureComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {testID, styles, theme, showLegacySidebar, collapsedThreadsEnabled, currentTeamId} = this.props;
|
||||
const {testID, styles, theme, showLegacySidebar, collapsedThreadsEnabled} = this.props;
|
||||
const {sections, categorySections, showIndicator} = this.state;
|
||||
|
||||
const paddingBottom = this.listContentPadding();
|
||||
@@ -561,7 +549,6 @@ export default class List extends PureComponent {
|
||||
<View
|
||||
style={styles.container}
|
||||
onLayout={this.onLayout}
|
||||
key={currentTeamId}
|
||||
>
|
||||
{collapsedThreadsEnabled && (
|
||||
<ThreadsSidebarEntry/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
|
||||
import React from 'react';
|
||||
import {intlShape} from 'react-intl';
|
||||
|
||||
@@ -185,7 +185,6 @@ export default class TextSetting extends PureComponent {
|
||||
<View style={[style.inputContainer, noediting]}>
|
||||
<View>
|
||||
<TextInput
|
||||
allowFontScaling={true}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
|
||||
@@ -17,7 +17,6 @@ const NavigationTypes = keyMirror({
|
||||
CLOSE_SETTINGS_SIDEBAR: null,
|
||||
BLUR_POST_DRAFT: null,
|
||||
CLOSE_SLIDE_UP: null,
|
||||
DRAWER: null,
|
||||
});
|
||||
|
||||
export default NavigationTypes;
|
||||
|
||||
@@ -27,8 +27,6 @@ export const NotificationLevels = {
|
||||
|
||||
export const NOTIFY_ALL_MEMBERS = 5;
|
||||
export const INDICATOR_BAR_HEIGHT = 38;
|
||||
export const JOIN_CALL_BAR_HEIGHT = 38;
|
||||
export const CURRENT_CALL_BAR_HEIGHT = 74;
|
||||
|
||||
export const CHANNEL_ITEM_LARGE_BADGE_MAX_WIDTH = 38;
|
||||
export const CHANNEL_ITEM_SMALL_BADGE_MAX_WIDTH = 32;
|
||||
@@ -107,11 +105,6 @@ const ViewTypes = keyMirror({
|
||||
|
||||
VIEWING_GLOBAL_THREADS_UNREADS: null,
|
||||
VIEWING_GLOBAL_THREADS_ALL: null,
|
||||
|
||||
THREAD_LAST_VIEWED_AT: null,
|
||||
|
||||
JOIN_CALL_BAR_VISIBLE: null,
|
||||
CURRENT_CALL_BAR_VISIBLE: null,
|
||||
});
|
||||
|
||||
const RequiredServer = {
|
||||
@@ -125,7 +118,7 @@ export default {
|
||||
...ViewTypes,
|
||||
RequiredServer,
|
||||
POST_VISIBILITY_CHUNK_SIZE: 60,
|
||||
CRT_CHUNK_SIZE: 30,
|
||||
CRT_CHUNK_SIZE: 60,
|
||||
FEATURE_TOGGLE_PREFIX: 'feature_enabled_',
|
||||
EMBED_PREVIEW: 'embed_preview',
|
||||
LINK_PREVIEW_DISPLAY: 'link_previews',
|
||||
|
||||
@@ -51,16 +51,5 @@ const WebsocketEvents = {
|
||||
SIDEBAR_CATEGORY_UPDATED: 'sidebar_category_updated',
|
||||
SIDEBAR_CATEGORY_DELETED: 'sidebar_category_deleted',
|
||||
SIDEBAR_CATEGORY_ORDER_UPDATED: 'sidebar_category_order_updated',
|
||||
CALLS_CHANNEL_ENABLED: 'custom_com.mattermost.calls_channel_enable_voice',
|
||||
CALLS_CHANNEL_DISABLED: 'custom_com.mattermost.calls_channel_disable_voice',
|
||||
CALLS_USER_CONNECTED: 'custom_com.mattermost.calls_user_connected',
|
||||
CALLS_USER_DISCONNECTED: 'custom_com.mattermost.calls_user_disconnected',
|
||||
CALLS_USER_MUTED: 'custom_com.mattermost.calls_user_muted',
|
||||
CALLS_USER_UNMUTED: 'custom_com.mattermost.calls_user_unmuted',
|
||||
CALLS_USER_VOICE_ON: 'custom_com.mattermost.calls_user_voice_on',
|
||||
CALLS_USER_VOICE_OFF: 'custom_com.mattermost.calls_user_voice_off',
|
||||
CALLS_CALL_START: 'custom_com.mattermost.calls_call_start',
|
||||
CALLS_SCREEN_ON: 'custom_com.mattermost.calls_user_screen_on',
|
||||
CALLS_SCREEN_OFF: 'custom_com.mattermost.calls_user_screen_off',
|
||||
};
|
||||
export default WebsocketEvents;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useWindowDimensions} from 'react-native';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
|
||||
import * as KeyChain from 'react-native-keychain';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
|
||||
import {DeviceTypes} from '@constants';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import CookieManager from '@react-native-cookies/cookies';
|
||||
|
||||
import {AppState, Dimensions, Keyboard, Linking, Platform} from 'react-native';
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import {Notifications} from 'react-native-notifications';
|
||||
|
||||
import * as Preferences from '@mm-redux/selectors/entities/preferences';
|
||||
import * as ViewSelectors from '@selectors/views';
|
||||
import Store from '@store/store';
|
||||
|
||||
@@ -50,118 +49,13 @@ describe('PushNotification', () => {
|
||||
// Clear channel1 notifications
|
||||
await PushNotification.clearChannelNotifications(channel1ID);
|
||||
|
||||
const deliveredNotifs = await Notifications.ios.getDeliveredNotifications();
|
||||
expect(deliveredNotifs.length).toBe(2);
|
||||
const channel1DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel1ID);
|
||||
const channel2DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel2ID);
|
||||
expect(channel1DeliveredNotifications.length).toBe(0);
|
||||
expect(channel2DeliveredNotifications.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should clear root posts only from the channel notifications when CRT is enabled', async () => {
|
||||
Store.redux = {
|
||||
getState: jest.fn(),
|
||||
};
|
||||
|
||||
Preferences.isCollapsedThreadsEnabled = jest.fn().mockImplementation(() => true);
|
||||
ViewSelectors.getBadgeCount = jest.fn().mockReturnValue(5);
|
||||
|
||||
const deliveredNotifications = [
|
||||
|
||||
// Three channel1 delivered notifications
|
||||
{
|
||||
identifier: 'channel1-1',
|
||||
channel_id: channel1ID,
|
||||
root_id: 'root-id-1',
|
||||
},
|
||||
{
|
||||
identifier: 'channel1-2',
|
||||
channel_id: channel1ID,
|
||||
},
|
||||
{
|
||||
identifier: 'channel1-3',
|
||||
channel_id: channel1ID,
|
||||
},
|
||||
|
||||
// Two channel2 delivered notifications
|
||||
{
|
||||
identifier: 'channel2-1',
|
||||
channel_id: channel2ID,
|
||||
root_id: 'root-id-2',
|
||||
},
|
||||
{
|
||||
identifier: 'channel2-2',
|
||||
channel_id: channel2ID,
|
||||
},
|
||||
];
|
||||
Notifications.setDeliveredNotifications(deliveredNotifications);
|
||||
|
||||
const notificationCount = deliveredNotifications.length;
|
||||
expect(notificationCount).toBe(5);
|
||||
|
||||
// Clear channel1 notifications
|
||||
await PushNotification.clearChannelNotifications(channel1ID);
|
||||
|
||||
const deliveredNotifs = await Notifications.ios.getDeliveredNotifications();
|
||||
expect(deliveredNotifs.length).toBe(3);
|
||||
const channel1DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel1ID);
|
||||
const channel2DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel2ID);
|
||||
expect(channel1DeliveredNotifications.length).toBe(1);
|
||||
expect(channel2DeliveredNotifications.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should clear all thread notifications', async () => {
|
||||
Store.redux = null;
|
||||
|
||||
ViewSelectors.getBadgeCount = jest.fn().mockReturnValue(5);
|
||||
|
||||
const root1ID = 'root-1-id';
|
||||
const root2ID = 'root-2-id';
|
||||
const root3ID = 'root-3-id';
|
||||
const deliveredNotifications = [
|
||||
|
||||
// Three channel1 delivered notifications
|
||||
{
|
||||
identifier: 'channel1-1',
|
||||
channel_id: channel1ID,
|
||||
root_id: root1ID,
|
||||
},
|
||||
{
|
||||
identifier: 'channel1-2',
|
||||
channel_id: channel1ID,
|
||||
root_id: root1ID,
|
||||
},
|
||||
{
|
||||
identifier: 'channel1-3',
|
||||
channel_id: channel1ID,
|
||||
root_id: root2ID,
|
||||
},
|
||||
|
||||
// Two channel2 delivered notifications
|
||||
{
|
||||
identifier: 'channel2-2',
|
||||
channel_id: channel2ID,
|
||||
},
|
||||
{
|
||||
identifier: 'channel2-2',
|
||||
channel_id: channel2ID,
|
||||
root_id: root3ID,
|
||||
},
|
||||
];
|
||||
Notifications.setDeliveredNotifications(deliveredNotifications);
|
||||
|
||||
const notificationCount = deliveredNotifications.length;
|
||||
expect(notificationCount).toBe(5);
|
||||
|
||||
// Clear channel1 notifications
|
||||
await PushNotification.clearChannelNotifications(channel1ID, root1ID);
|
||||
|
||||
const deliveredNotifs = await Notifications.ios.getDeliveredNotifications();
|
||||
expect(deliveredNotifs.length).toBe(3);
|
||||
const channel1DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel1ID);
|
||||
const channel2DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel2ID);
|
||||
expect(channel1DeliveredNotifications.length).toBe(1);
|
||||
expect(channel2DeliveredNotifications.length).toBe(2);
|
||||
await Notifications.ios.getDeliveredNotifications(async (deliveredNotifs) => {
|
||||
expect(deliveredNotifs.length).toBe(2);
|
||||
const channel1DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel1ID);
|
||||
const channel2DeliveredNotifications = deliveredNotifs.filter((n) => n.channel_id === channel2ID);
|
||||
expect(channel1DeliveredNotifications.length).toBe(0);
|
||||
expect(channel2DeliveredNotifications.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear all notifications', async () => {
|
||||
@@ -169,7 +63,7 @@ describe('PushNotification', () => {
|
||||
const cancelAllLocalNotifications = jest.spyOn(PushNotification, 'cancelAllLocalNotifications');
|
||||
|
||||
PushNotification.clearNotifications();
|
||||
expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(0);
|
||||
await expect(setApplicationIconBadgeNumber).toHaveBeenCalledWith(0);
|
||||
expect(Notifications.ios.setBadgeCount).toHaveBeenCalledWith(0);
|
||||
expect(cancelAllLocalNotifications).toHaveBeenCalled();
|
||||
expect(Notifications.cancelAllLocalNotifications).toHaveBeenCalled();
|
||||
|
||||
@@ -46,8 +46,6 @@ const NOTIFICATION_TYPE = {
|
||||
interface NotificationWithChannel extends Notification {
|
||||
identifier: string;
|
||||
channel_id: string;
|
||||
post_id: string;
|
||||
root_id: string;
|
||||
}
|
||||
|
||||
class PushNotifications {
|
||||
@@ -63,13 +61,6 @@ class PushNotifications {
|
||||
this.getInitialNotification();
|
||||
}
|
||||
|
||||
getNotifications = async (): Promise<NotificationWithChannel[]> => {
|
||||
if (Platform.OS === 'android') {
|
||||
return AndroidNotificationPreferences.getDeliveredNotifications();
|
||||
}
|
||||
return Notifications.ios.getDeliveredNotifications() as Promise<NotificationWithChannel[]>;
|
||||
}
|
||||
|
||||
cancelAllLocalNotifications() {
|
||||
Notifications.cancelAllLocalNotifications();
|
||||
}
|
||||
@@ -84,54 +75,34 @@ class PushNotifications {
|
||||
}
|
||||
};
|
||||
|
||||
clearChannelNotifications = async (channelId: string, rootId?: string) => {
|
||||
const notifications = await this.getNotifications();
|
||||
clearChannelNotifications = async (channelId: string) => {
|
||||
if (Platform.OS === 'android') {
|
||||
const notifications = await AndroidNotificationPreferences.getDeliveredNotifications();
|
||||
const notificationForChannel = notifications.find((n: NotificationWithChannel) => n.channel_id === channelId);
|
||||
if (notificationForChannel) {
|
||||
AndroidNotificationPreferences.removeDeliveredNotifications(channelId);
|
||||
}
|
||||
} else {
|
||||
const ids: string[] = [];
|
||||
const notifications = await Notifications.ios.getDeliveredNotifications();
|
||||
|
||||
let collapsedThreadsEnabled = false;
|
||||
if (Store.redux) {
|
||||
collapsedThreadsEnabled = isCollapsedThreadsEnabled(Store.redux.getState());
|
||||
}
|
||||
//set the badge count to the total amount of notifications present in the not-center
|
||||
let badgeCount = notifications.length;
|
||||
|
||||
const clearThreads = Boolean(rootId);
|
||||
|
||||
const notificationIds: string[] = [];
|
||||
for (let i = 0; i < notifications.length; i++) {
|
||||
const notification = notifications[i];
|
||||
if (notification.channel_id === channelId) {
|
||||
let doesNotificationMatch = true;
|
||||
if (clearThreads) {
|
||||
doesNotificationMatch = notification.root_id === rootId;
|
||||
} else if (collapsedThreadsEnabled) {
|
||||
// Do not match when CRT is enabled BUT post is not a root post
|
||||
doesNotificationMatch = !notification.root_id;
|
||||
}
|
||||
|
||||
if (doesNotificationMatch) {
|
||||
notificationIds.push(notification.identifier || notification.post_id);
|
||||
|
||||
// For Android, We just need one matching notification to clear the notifications
|
||||
if (Platform.OS === 'android') {
|
||||
break;
|
||||
}
|
||||
for (let i = 0; i < notifications.length; i++) {
|
||||
const notification = notifications[i] as NotificationWithChannel;
|
||||
if (notification.channel_id === channelId) {
|
||||
ids.push(notification.identifier);
|
||||
badgeCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
//set the badge count to the total amount of notifications present in the not-center
|
||||
const badgeCount = notifications.length - notificationIds.length;
|
||||
if (ids.length) {
|
||||
Notifications.ios.removeDeliveredNotifications(ids);
|
||||
}
|
||||
|
||||
this.setBadgeCountByMentions(badgeCount);
|
||||
}
|
||||
|
||||
if (!notificationIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
AndroidNotificationPreferences.removeDeliveredNotifications(channelId, rootId, collapsedThreadsEnabled);
|
||||
} else {
|
||||
Notifications.ios.removeDeliveredNotifications(notificationIds);
|
||||
}
|
||||
}
|
||||
|
||||
setBadgeCountByMentions = (initialBadge = 0) => {
|
||||
|
||||
@@ -105,9 +105,7 @@ Navigation.events().registerAppLaunchedListener(() => {
|
||||
});
|
||||
|
||||
export function componentDidAppearListener({componentId}) {
|
||||
if (componentId.indexOf('!screen') !== 0) {
|
||||
EphemeralStore.addNavigationComponentId(componentId);
|
||||
}
|
||||
EphemeralStore.addNavigationComponentId(componentId);
|
||||
|
||||
switch (componentId) {
|
||||
case 'MainSidebar':
|
||||
|
||||
@@ -52,12 +52,6 @@ describe('componentDidAppearListener', () => {
|
||||
expect(EventEmitter.emit).toHaveBeenCalledTimes(1);
|
||||
expect(EventEmitter.emit).toHaveBeenCalledWith(NavigationTypes.BLUR_POST_DRAFT);
|
||||
});
|
||||
|
||||
it('should not add componentIds starting with "!screen" to the store as they are not screens', () => {
|
||||
const componentId = '!screen';
|
||||
componentDidAppearListener({componentId});
|
||||
expect(EphemeralStore.addNavigationComponentId).not.toHaveBeenCalledWith(componentId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('componentDidDisappearListener', () => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import CallsTypes from '@mmproducts/calls/store/action_types/calls';
|
||||
|
||||
import AppsTypes from './apps';
|
||||
import BotTypes from './bots';
|
||||
import ChannelCategoryTypes from './channel_categories';
|
||||
@@ -45,5 +43,4 @@ export {
|
||||
ThreadTypes,
|
||||
RemoteClusterTypes,
|
||||
AppsTypes,
|
||||
CallsTypes,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
import {isEqual} from 'lodash';
|
||||
|
||||
import {Client4} from '@client/rest';
|
||||
import {getUser} from '@components/autocomplete/slash_suggestion/app_command_parser/app_command_parser_dependencies';
|
||||
@@ -15,7 +16,7 @@ import {ActionFunc, batchActions, DispatchFunc, GetStateFunc} from '@mm-redux/ty
|
||||
import {CategorySorting, ChannelCategory, OrderedChannelCategories} from '@mm-redux/types/channel_categories';
|
||||
import {Channel} from '@mm-redux/types/channels';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
import {$ID, RelationOneToMany} from '@mm-redux/types/utilities';
|
||||
import {$ID, IDMappedObjects, RelationOneToMany} from '@mm-redux/types/utilities';
|
||||
import {insertMultipleWithoutDuplicates, insertWithoutDuplicates, removeItem} from '@mm-redux/utils/array_utils';
|
||||
import {getUserIdFromChannelName} from '@mm-redux/utils/channel_utils';
|
||||
|
||||
@@ -31,23 +32,10 @@ export function collapseCategory(categoryId: string) {
|
||||
return setCategoryCollapsed(categoryId, true);
|
||||
}
|
||||
|
||||
export function setCategoryCollapsed(categoryId: string, collapsed: boolean): ActionFunc {
|
||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||
const state = getState();
|
||||
|
||||
const category = getCategory(state, categoryId);
|
||||
const patchedCategory = {
|
||||
...category,
|
||||
collapsed,
|
||||
};
|
||||
|
||||
dispatch({
|
||||
type: ChannelCategoryTypes.RECEIVED_CATEGORY,
|
||||
data: patchedCategory,
|
||||
});
|
||||
|
||||
return {data: patchedCategory};
|
||||
};
|
||||
export function setCategoryCollapsed(categoryId: string, collapsed: boolean) {
|
||||
return patchCategory(categoryId, {
|
||||
collapsed,
|
||||
});
|
||||
}
|
||||
|
||||
export function setCategorySorting(categoryId: string, sorting: CategorySorting) {
|
||||
@@ -154,11 +142,20 @@ export function fetchMyCategories(teamId: string) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
// Remove collapse state from server data
|
||||
data.categories = data.categories.map((cat) => {
|
||||
delete cat.collapsed;
|
||||
return cat;
|
||||
});
|
||||
/*
|
||||
* Make sure that we don't dispatch an unnecessary update after fetching
|
||||
*/
|
||||
const categoriesInState = getState().entities.channelCategories.byId;
|
||||
const mappedCats = data.order.reduce((prev, categoryId) => {
|
||||
return {
|
||||
...prev,
|
||||
[categoryId]: data.categories.find((category) => category.id === categoryId),
|
||||
};
|
||||
}, {} as IDMappedObjects<ChannelCategory>);
|
||||
|
||||
if (isEqual(mappedCats, categoriesInState)) {
|
||||
return {data: false};
|
||||
}
|
||||
|
||||
return dispatch(batchActions([
|
||||
{
|
||||
@@ -202,7 +199,7 @@ export function addChannelToInitialCategory(channel: Channel, setOnServer = fals
|
||||
// Get the user ids in the channel
|
||||
const allUsersInChannels: RelationOneToMany<Channel, UserProfile> = getUserIdsInChannels(state);
|
||||
const allUsersInGMChannel = Array.from(allUsersInChannels[channel.id] || []);
|
||||
const usersInGMChannel: string[] = allUsersInGMChannel.filter((u: string) => u !== currentUserId);
|
||||
const usersInGMChannel: Array<string> = allUsersInGMChannel.filter((u: string) => u !== currentUserId);
|
||||
|
||||
// Filter and see if there are any missing in our state
|
||||
const missingUsers = usersInGMChannel.filter((id) => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as calls from '@mmproducts/calls/store/actions/calls';
|
||||
|
||||
import * as bots from './bots';
|
||||
import * as channels from './channels';
|
||||
import * as emojis from './emojis';
|
||||
@@ -39,6 +37,5 @@ export {
|
||||
timezone,
|
||||
users,
|
||||
remoteCluster,
|
||||
calls,
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user