Compare commits

..

4 Commits

Author SHA1 Message Date
Elias Nahum
86508004cf update fastlane 2022-01-13 11:54:00 -03:00
Elias Nahum
4f5bcdcb59 match type 2021-10-01 21:14:18 -03:00
Elias Nahum
e25e179e42 Merge branch 'master' into match 2021-10-01 21:00:04 -03:00
Elias Nahum
62e1fdac7d Get Apple API key for match adhoc lane 2021-07-06 13:58:35 -04:00
346 changed files with 10209 additions and 27538 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,14 @@
# Mattermost Mobile App
[![Mattermost](https://user-images.githubusercontent.com/7205829/136108314-75cd2e1f-4147-4cfa-a16c-9b3b0313ea25.png)](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).

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FormattedRelativeTime should match snapshot 1`] = `
<Text>
a few seconds ago
</Text>
`;

View File

@@ -113,6 +113,7 @@ export default class AtMention extends React.PureComponent {
BottomSheet.showBottomSheetWithOptions({
options: [actionText, cancelText],
cancelButtonIndex: 1,
}, (value) => {
if (value !== 1) {
this.handleCopyMention();

View File

@@ -66,7 +66,7 @@ const GroupMentionItem = (props) => {
>
<View style={style.rowPicture}>
<CompassIcon
name='account-multiple-outline'
name='account-group-outline'
style={style.rowIcon}
/>
</View>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -165,7 +165,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
container: {
flex: 1,
flexDirection: 'row',
paddingHorizontal: 15,
marginHorizontal: 10,
overflow: 'hidden',
},
profileContainer: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,6 @@
exports[`CameraButton should match snapshot 1`] = `
<View
accessibilityState={
Object {
"disabled": false,
}
}
accessible={true}
collapsable={false}
focusable={true}

View File

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

View File

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

View File

@@ -2,11 +2,6 @@
exports[`ImageQuickAction should match snapshot 1`] = `
<View
accessibilityState={
Object {
"disabled": false,
}
}
accessible={true}
collapsable={false}
focusable={true}

View File

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

View File

@@ -18,7 +18,7 @@ exports[`MoreMessagesButton should match snapshot 1`] = `
Object {
"transform": Array [
Object {
"translateY": -550,
"translateY": -438,
},
],
},

View File

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

View File

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

View File

@@ -200,6 +200,4 @@ const DocumentFile = forwardRef<DocumentFileRef, DocumentFileProps>(({background
);
});
DocumentFile.displayName = 'DocumentFile';
export default injectIntl(DocumentFile, {withRef: true});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,7 +33,6 @@ exports[`SearchBar should match snapshot 1`] = `
}
>
<ForwardRef(Themed.SearchBar)
allowFontScaling={true}
autoCapitalize="auto-capitalize"
autoCorrect={false}
autoFocus={true}

View File

@@ -323,7 +323,6 @@ export default class Search extends PureComponent {
]}
>
<SearchBar
allowFontScaling={true}
testID={searchInputTestID}
autoCapitalize={this.props.autoCapitalize}
autoCorrect={false}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -83,7 +83,6 @@ function mapStateToProps(state) {
showLegacySidebar,
unreadsOnTop,
currentChannelId,
currentTeamId,
};
}

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,6 @@ const NavigationTypes = keyMirror({
CLOSE_SETTINGS_SIDEBAR: null,
BLUR_POST_DRAFT: null,
CLOSE_SLIDE_UP: null,
DRAWER: null,
});
export default NavigationTypes;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) => {

View File

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

View File

@@ -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', () => {

View File

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

View File

@@ -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) => {

View File

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