Compare commits

..

42 Commits

Author SHA1 Message Date
Elias Nahum
56e36b0468 Version Bump to 57 2017-10-05 17:44:04 -03:00
Elias Nahum
b5b57085e5 Version Bump to 57 2017-10-05 17:41:26 -03:00
Elias Nahum
e082b42947 Fix crashes when user hasn't loaded and when postDraft was undefined 2017-10-05 16:59:07 -03:00
Elias Nahum
77fdaa9058 Version Bump to 56 2017-10-05 12:24:57 -03:00
Elias Nahum
774f1a1b47 Version Bump to 56 2017-10-05 12:20:48 -03:00
Elias Nahum
192e5093c1 Fix postTextBox at_mention autocomplete 2017-10-05 11:51:41 -03:00
Elias Nahum
52ba404b8e Version Bump to 55 2017-10-05 10:03:11 -03:00
Elias Nahum
8249080304 Version Bump to 55 2017-10-05 09:59:45 -03:00
Elias Nahum
b6c0d47d18 Fix Android crash on push notification 2017-10-05 09:38:15 -03:00
enahum
031876fb77 RN-382 Refactor at_mention & channel_mention autocomplete (#988)
* RN-382 Refactor at_mention & channel_mention autocomplete

* Feedback review

* If the term changes always trigger a request
2017-10-04 13:36:51 -07:00
Harrison Healey
ac0ac22f39 RN-383 Fixed new messages indicator (#990) 2017-10-04 08:57:48 -07:00
enahum
bed81ad514 Version Bump to 54 (#985) 2017-10-02 15:53:35 -03:00
enahum
642dd299c6 Version Bump to 54 (#984) 2017-10-02 15:51:15 -03:00
enahum
2633060a7f Remove context menu for system and ephemeral messages (#981) 2017-10-02 15:27:51 -03:00
enahum
50369d0c28 RN-362 Do not crash when leaving channels (#980) 2017-10-02 15:27:38 -03:00
enahum
4ef308469d Version Bump to 53 (#979) 2017-10-02 13:27:31 -03:00
enahum
f2394ba8df IOS Version Bump to 53 (#978)
* iOS deployment target 9.3

* Version Bump to 53
2017-10-02 13:25:22 -03:00
enahum
cc55b03e75 Make Post time format respect user display setting (#977) 2017-10-02 08:39:02 -07:00
enahum
82b3dcc1f6 Set minSdkVersion to 21 (Android 5.0) (#976)
* Set minSdkVersion to 21 (Android 5.0)

* Update README
2017-10-02 12:22:28 -03:00
enahum
13922e3764 Fix drawer according to number of teams (#974) 2017-10-02 12:22:14 -03:00
enahum
0df3c7428a Fix search issues (#973) 2017-10-02 12:21:56 -03:00
Harrison Healey
8e526b61ed RN-379 Get posts since last websocket disconnect when viewing channel (#971)
* RN-379 Added websocket state to device state

* Fixed view store blacklist

* RN-379 Get posts since last websocket disconnect when viewing channel

* Used Date.now instead of new Date().getTime()
2017-10-02 12:21:39 -03:00
Harrison Healey
c93f04a708 RN-268 Fixed teams list not always rerendering when team member changes (#970)
* RN-268 Fixed teams list not always rerendering when team member changes

* Removed unused prop from ChannelDrawer

* RN-268 Passed fewer props into TeamsListItem
2017-09-29 12:46:03 -03:00
enahum
e3761fc529 Version Bump to 52 (#968) 2017-09-28 15:55:40 -03:00
enahum
72fef11496 Version Bump to 52 (#967) 2017-09-28 15:55:27 -03:00
enahum
6fdd58b481 Fix blank screen when user has no teams (#965) 2017-09-28 15:16:35 -03:00
Harrison Healey
ad2d126ec0 RN-364 Fixed switch teams button rendering with only one team (#964) 2017-09-28 14:53:41 -03:00
Harrison Healey
76eb5d06fd Fixed drawer rendering empty if the user is only on one team (#963) 2017-09-28 14:53:27 -03:00
enahum
1e434346ae Multiple performance improvements (#956)
* Update fastlane

* Multiple performance improvements

* Feedback review

* Feedback review
2017-09-28 12:54:32 -03:00
enahum
a694122ffd Fix post additional content (#962)
* Fix post additional content

* Feedback review
2017-09-28 12:02:41 -03:00
enahum
0c3bb89832 Fix SSO login (#960) 2017-09-28 11:32:26 -03:00
enahum
78e6b8d5a3 Fix image preview initial scroll (#959) 2017-09-28 11:31:44 -03:00
enahum
ae7c566375 Fix android search autocomplete (#958) 2017-09-28 11:17:58 -03:00
enahum
6f260bf4c7 RN-320 Tapping enter in channel header/purpose keeps the keyboard open (#961) 2017-09-28 11:17:34 -03:00
Harrison Healey
ff65b52618 Added selector for getting sorted teams list (#954) 2017-09-28 11:17:22 -03:00
enahum
2f47d7db2e Include iOS marketing icon (#957) 2017-09-28 06:02:41 -03:00
Harrison Healey
b8e450ba85 RN-369 Added a maximum depth to the markdown renderer (#942)
* RN-369 Added a maximum depth to the markdown renderer

* Updated yarn.lock
2017-09-27 17:49:38 -07:00
Harrison Healey
f2533bd650 RN-349 Mark channel as read when switching teams and opening the app (#953)
* RN-349 Mark current channel as read when opening the app

* RN-349 Mark channels as read when switching teams

* Moved markChannelAsRead into handleTeamChange action
2017-09-27 15:34:09 -03:00
enahum
f9419a7746 Version Bump to 51 (#938) 2017-09-27 10:41:35 -03:00
enahum
978c80bef1 Version Bump to 51 (#937) 2017-09-27 10:41:21 -03:00
enahum
6e1d8471f7 translations PR 20170925 (#940) 2017-09-27 07:47:15 -03:00
Elias Nahum
fa9110d9d7 UpsideDown required by apple 2017-09-25 15:44:35 -03:00
221 changed files with 2725 additions and 6184 deletions

View File

@@ -1,10 +1,5 @@
{
"presets": [ "react-native" ],
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
},
"plugins": [
["module-resolver", {
"root": ["./src", "."],

View File

@@ -6,10 +6,8 @@ import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import android.provider.Settings;
import android.support.annotation.NonNull;
@@ -46,7 +44,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.util.List;
import com.facebook.react.modules.core.PermissionListener;
import com.facebook.react.modules.core.PermissionAwareActivity;
@@ -199,9 +196,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
public void doOnCancel()
{
if (this.callback != null) {
responseHelper.invokeCancel(this.callback);
}
responseHelper.invokeCancel(callback);
}
public void launchCamera()
@@ -226,7 +221,6 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
return;
}
this.callback = callback;
this.options = options;
if (!permissionsCheck(currentActivity, callback, REQUEST_PERMISSIONS_FOR_CAMERA))
@@ -257,12 +251,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
final File original = createNewFile(reactContext, this.options, false);
imageConfig = imageConfig.withOriginalFile(original);
if (imageConfig.original != null) {
cameraCaptureURI = RealPathUtil.compatUriFromFile(reactContext, imageConfig.original);
}else {
responseHelper.invokeError(callback, "Couldn't get file path for photo");
return;
}
cameraCaptureURI = RealPathUtil.compatUriFromFile(reactContext, imageConfig.original);
if (cameraCaptureURI == null)
{
responseHelper.invokeError(callback, "Couldn't get file path for photo");
@@ -277,16 +266,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
return;
}
// Workaround for Android bug.
// grantUriPermission also needed for KITKAT,
// see https://code.google.com/p/android/issues/detail?id=76683
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
List<ResolveInfo> resInfoList = reactContext.getPackageManager().queryIntentActivities(cameraIntent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
reactContext.grantUriPermission(packageName, cameraCaptureURI, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
}
this.callback = callback;
try
{
@@ -314,7 +294,6 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
}
this.options = options;
this.callback = callback;
if (!permissionsCheck(currentActivity, callback, REQUEST_PERMISSIONS_FOR_LIBRARY))
{
@@ -335,7 +314,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
{
requestCode = REQUEST_LAUNCH_IMAGE_LIBRARY;
libraryIntent = new Intent(Intent.ACTION_PICK,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
}
if (libraryIntent.resolveActivity(reactContext.getPackageManager()) == null)
@@ -344,6 +323,8 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
return;
}
this.callback = callback;
try
{
currentActivity.startActivityForResult(libraryIntent, requestCode);
@@ -590,9 +571,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
innerActivity.startActivityForResult(intent, 1);
}
});
if (dialog != null) {
dialog.show();
}
dialog.show();
return false;
}
else
@@ -627,7 +606,7 @@ public class ImagePickerModule extends ReactContextBaseJavaModule
private boolean isCameraAvailable() {
return reactContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)
|| reactContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
|| reactContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
}
private @NonNull String getRealPathFromURI(@NonNull final Uri uri) {

View File

@@ -86,7 +86,7 @@ run-android: | check-device-android start prepare-android-build
@echo Running Android app in development
@react-native run-android --no-packager
test: | pre-run check-style
test: pre-run
@yarn test
check-style: .yarninstall
@@ -120,9 +120,7 @@ post-install:
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-messageformat/package.json
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
@sed -i'' -e 's|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_FULL_USER);|g' node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/params/Orientation.java
@cd ./node_modules/react-native-svg/ios && rm -rf PerformanceBezier && git clone https://github.com/adamwulf/PerformanceBezier.git
@cd ./node_modules/mattermost-redux && yarn run build
@sed -i'' -e 's|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_SENSOR);|g' node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/params/Orientation.java
start-packager:
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
@@ -156,7 +154,7 @@ endif
do-build-ios:
@echo "Building ios $(ios_target) app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios $(ios_target)
@cd fastlane && NODE_ENV=production bundle exec fastlane ios $(ios_target)
build-ios: | check-ios-target pre-run check-style start-packager do-build-ios stop-packager
@@ -180,7 +178,7 @@ prepare-android-build:
do-build-android:
@echo "Building android $(android_target) app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android $(android_target)
@cd fastlane && NODE_ENV=production bundle exec fastlane android $(android_target)
build-android: | check-android-target pre-run check-style start-packager prepare-android-build do-build-android stop-packager

View File

@@ -84,8 +84,7 @@ Follow the [React Native Getting Started Guide](https://facebook.github.io/react
$ cd watchman
$ git checkout master
$ ./autogen.sh
$ ./configure
$ make
$ ./configure make
$ sudo make install
```
Configure your kernel to accept a lot of file watches, using a command like:

View File

@@ -95,8 +95,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion 21
targetSdkVersion 23
versionCode 63
versionName "1.4.0"
versionCode 57
versionName "1.3.0"
multiDexEnabled true
ndk {
abiFilters "armeabi-v7a", "x86"
@@ -154,7 +154,6 @@ android {
dependencies {
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:25.0.1"
compile 'com.android.support:percent:25.3.1'
compile "com.facebook.react:react-native:+" // From node_modules
compile project(':react-native-navigation')
compile project(':react-native-image-picker')

View File

@@ -16,11 +16,8 @@ import android.os.Build;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.provider.Settings.System;
import java.util.LinkedHashMap;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.lang.reflect.Field;
import com.wix.reactnativenotifications.core.notification.PushNotification;
@@ -43,7 +40,7 @@ public class CustomPushNotification extends PushNotification {
public static final String NOTIFICATION_REPLIED_EVENT_NAME = "notificationReplied";
private static LinkedHashMap<String,Integer> channelIdToNotificationCount = new LinkedHashMap<String,Integer>();
private static LinkedHashMap<String,List<Bundle>> channelIdToNotification = new LinkedHashMap<String,List<Bundle>>();
private static LinkedHashMap<String,ArrayList<Bundle>> channelIdToNotification = new LinkedHashMap<String,ArrayList<Bundle>>();
private static AppLifecycleFacade lifecycleFacade;
private static Context context;
@@ -77,16 +74,14 @@ public class CustomPushNotification extends PushNotification {
channelIdToNotificationCount.put(channelId, count);
Object bundleArray = channelIdToNotification.get(channelId);
List list = null;
ArrayList list = null;
if (bundleArray == null) {
list = Collections.synchronizedList(new ArrayList(0));
list = new ArrayList(0);
} else {
list = Collections.synchronizedList((List)bundleArray);
}
synchronized (list) {
list.add(0, data);
channelIdToNotification.put(channelId, list);
list = (ArrayList)bundleArray;
}
list.add(0, data);
channelIdToNotification.put(channelId, list);
}
if ("clear".equals(type)) {
@@ -197,7 +192,7 @@ public class CustomPushNotification extends PushNotification {
String summaryTitle = String.format("%s (%d)", title, numMessages);
Notification.InboxStyle style = new Notification.InboxStyle();
List<Bundle> list = new ArrayList<Bundle>(channelIdToNotification.get(channelId));
ArrayList<Bundle> list = (ArrayList<Bundle>) channelIdToNotification.get(channelId);
for (Bundle data : list){
String msg = data.getString("message");
@@ -259,7 +254,7 @@ public class CustomPushNotification extends PushNotification {
notification.setSound(Uri.parse(soundUri), AudioManager.STREAM_NOTIFICATION);
}
} else {
Uri defaultUri = System.DEFAULT_NOTIFICATION_URI;
Uri defaultUri = RingtoneManager.getActualDefaultRingtoneUri(mContext, RingtoneManager.TYPE_NOTIFICATION);
notification.setSound(defaultUri, AudioManager.STREAM_NOTIFICATION);
}

View File

@@ -2,9 +2,62 @@ package com.mattermost.rnbeta;
import com.reactnativenavigation.controllers.SplashActivity;
import java.lang.ref.WeakReference;
import android.content.Context;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.graphics.Color;
import android.widget.TextView;
import android.view.ViewGroup.LayoutParams;
import android.view.Gravity;
import android.util.TypedValue;
public class MainActivity extends SplashActivity {
@Override
public int getSplashLayout() {
return R.layout.launch_screen;
}
private static ImageView imageView;
private static WeakReference<MainActivity> wr_activity;
protected static MainActivity getActivity() {
return wr_activity.get();
}
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
// @Override
// protected String getMainComponentName() {
// return "Mattermost";
// }
@Override
public LinearLayout createSplashLayout() {
wr_activity = new WeakReference<>(this);
LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
Context context = getActivity();
final int drawableId = getImageId();
NotificationsLifecycleFacade.getInstance().LoadManagedConfig(getActivity());
imageView = new ImageView(context);
imageView.setImageResource(drawableId);
imageView.setLayoutParams(layoutParams);
imageView.setScaleType(ImageView.ScaleType.CENTER);
LinearLayout view = new LinearLayout(this);
view.setBackgroundColor(Color.parseColor("#FFFFFF"));
view.setGravity(Gravity.CENTER);
view.addView(imageView);
return view;
}
private static int getImageId() {
int drawableId = getActivity().getResources().getIdentifier("splash", "drawable", getActivity().getClass().getPackage().getName());
if (drawableId == 0) {
drawableId = getActivity().getResources().getIdentifier("splash", "drawable", getActivity().getPackageName());
}
return drawableId;
}
}

View File

@@ -6,7 +6,6 @@ import android.content.Context;
import android.os.Bundle;
import com.facebook.react.ReactApplication;
import com.horcrux.svg.SvgPackage;
import com.inprogress.reactnativeyoutube.ReactNativeYouTube;
import io.sentry.RNSentryPackage;
import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage;
@@ -24,6 +23,7 @@ import com.gnet.bottomsheet.RNBottomSheetPackage;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
import com.psykar.cookiemanager.CookieManagerPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.horcrux.svg.SvgPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.github.yamill.orientation.OrientationPackage;
import com.reactnativenavigation.NavigationApplication;

View File

@@ -1,19 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.percent.PercentRelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:gravity="center_horizontal"
tools:context=".SplashScreenActivity">
<ImageView
android:id="@+id/imgLogo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:src="@drawable/splash" />
</android.support.percent.PercentRelativeLayout>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="white">#FFFFFF</color>
</resources>

View File

@@ -3,9 +3,7 @@
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:windowIsTranslucent">false</item>
<item name="android:windowBackground">@color/white</item>
<item name="android:colorBackground">@color/white</item>
<item name="android:windowIsTranslucent">true</item>
</style>
</resources>

View File

@@ -27,9 +27,9 @@ include ':reactnativenotifications'
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android')
include ':app'
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-orientation'
project(':react-native-orientation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-orientation/android')
include ':react-native-linear-gradient'
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')

View File

@@ -8,6 +8,7 @@ import {ViewTypes} from 'app/constants';
import {UserTypes} from 'mattermost-redux/action_types';
import {
fetchMyChannelsAndMembers,
getChannelStats,
selectChannel,
leaveChannel as serviceLeaveChannel,
unfavoriteChannel
@@ -18,19 +19,18 @@ import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
import {General, Preferences} from 'mattermost-redux/constants';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {
getChannelByName,
getDirectChannelName,
getUserIdFromChannelName,
isDirectChannelVisible,
isGroupChannelVisible,
isDirectChannel,
isGroupChannel
} from 'mattermost-redux/utils/channel_utils';
import {getLastCreateAt} from 'mattermost-redux/utils/post_utils';
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
import {isDirectChannelVisible, isGroupChannelVisible} from 'app/utils/channels';
const MAX_POST_TRIES = 3;
export function loadChannelsIfNecessary(teamId) {
@@ -180,17 +180,15 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
};
}
export async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_POST_TRIES) {
async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_POST_TRIES) {
for (let i = 0; i < maxTries; i++) {
const {data} = await action(dispatch, getState);
const posts = await action(dispatch, getState);
if (data) {
dispatch(setChannelRetryFailed(false));
return data;
if (posts) {
return posts;
}
}
dispatch(setChannelRetryFailed(true));
return null;
}
@@ -223,8 +221,7 @@ export function selectInitialChannel(teamId) {
const {channels, myMembers} = state.entities.channels;
const {currentUserId} = state.entities.users;
const {myPreferences} = state.entities.preferences;
const lastChannelForTeam = state.views.team.lastChannelForTeam[teamId];
const lastChannelId = lastChannelForTeam && lastChannelForTeam.length ? lastChannelForTeam[0] : '';
const lastChannelId = state.views.team.lastChannelForTeam[teamId] || '';
const lastChannel = channels[lastChannelId];
const isDMVisible = lastChannel && lastChannel.type === General.DM_CHANNEL &&
@@ -258,84 +255,28 @@ export function handleSelectChannel(channelId) {
return async (dispatch, getState) => {
const {currentTeamId} = getState().entities.teams;
loadPostsIfNecessaryWithRetry(channelId)(dispatch, getState);
selectChannel(channelId)(dispatch, getState);
dispatch(batchActions([
{
type: ViewTypes.SET_INITIAL_POST_VISIBILITY,
data: channelId
},
setChannelLoading(false),
{
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
teamId: currentTeamId,
channelId
}
]), 'BATCH_CHANNEL_LOADED');
dispatch(setChannelLoading(false));
dispatch({
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
teamId: currentTeamId,
channelId
});
getChannelStats(channelId)(dispatch, getState);
};
}
export function handlePostDraftChanged(channelId, draft) {
export function handlePostDraftChanged(channelId, postDraft) {
return async (dispatch, getState) => {
dispatch({
type: ViewTypes.POST_DRAFT_CHANGED,
channelId,
draft
postDraft
}, getState);
};
}
export function handlePostDraftSelectionChanged(channelId, cursorPosition) {
return {
type: ViewTypes.POST_DRAFT_SELECTION_CHANGED,
channelId,
cursorPosition
};
}
export function insertToDraft(value) {
return (dispatch, getState) => {
const state = getState();
const channelId = getCurrentChannelId(state);
const threadId = state.entities.posts.selectedPostId;
let draft;
let cursorPosition;
let action;
if (state.views.thread.drafts[threadId]) {
const threadDraft = state.views.thread.drafts[threadId];
draft = threadDraft.draft;
cursorPosition = threadDraft.cursorPosition;
action = {
type: ViewTypes.COMMENT_DRAFT_CHANGED,
rootId: threadId
};
} else if (state.views.channel.drafts[channelId]) {
const channelDraft = state.views.channel.drafts[channelId];
draft = channelDraft.draft;
cursorPosition = channelDraft.cursorPosition;
action = {
type: ViewTypes.POST_DRAFT_CHANGED,
channelId
};
}
let nextDraft = `${value}`;
if (cursorPosition > 0) {
const beginning = draft.slice(0, cursorPosition);
const end = draft.slice(cursorPosition);
nextDraft = `${beginning}${value}${end}`;
}
if (action && nextDraft !== draft) {
dispatch({
...action,
draft: nextDraft
});
}
};
}
export function toggleDMChannel(otherUserId, visible) {
return async (dispatch, getState) => {
const state = getState();
@@ -399,11 +340,8 @@ export function closeGMChannel(channel) {
}
export function refreshChannelWithRetry(channelId) {
return async (dispatch, getState) => {
dispatch(setChannelRefreshing(true));
const posts = await retryGetPostsAction(getPosts(channelId), dispatch, getState);
dispatch(setChannelRefreshing(false));
return posts;
return (dispatch, getState) => {
return retryGetPostsAction(getPosts(channelId), dispatch, getState);
};
}
@@ -431,10 +369,10 @@ export function setChannelRefreshing(loading = true) {
};
}
export function setChannelRetryFailed(failed = true) {
export function setPostTooltipVisible(visible = true) {
return {
type: ViewTypes.SET_CHANNEL_RETRY_FAILED,
failed
type: ViewTypes.POST_TOOLTIP_VISIBLE,
visible
};
}
@@ -466,14 +404,13 @@ export function increasePostVisibility(channelId, focusedPostId) {
const page = Math.floor(currentPostVisibility / ViewTypes.POST_VISIBILITY_CHUNK_SIZE);
let result;
let posts;
if (focusedPostId) {
result = await getPostsBefore(channelId, focusedPostId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
posts = await getPostsBefore(channelId, focusedPostId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
} else {
result = await getPosts(channelId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
posts = await getPosts(channelId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
}
const posts = result.data;
if (posts) {
// make sure to increment the posts visibility
// only if we got results

View File

@@ -1,10 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';
export function setLastUpgradeCheck() {
return {
type: ViewTypes.SET_LAST_UPGRADE_CHECK
};
}

View File

@@ -12,7 +12,7 @@ export function handleCreateChannel(displayName, purpose, header, type) {
const state = getState();
const currentUserId = getCurrentUserId(state);
const teamId = getCurrentTeamId(state);
const channel = {
let channel = {
team_id: teamId,
name: cleanUpUrlable(displayName),
display_name: displayName,
@@ -21,10 +21,10 @@ export function handleCreateChannel(displayName, purpose, header, type) {
type
};
const {data} = await createChannel(channel, currentUserId)(dispatch, getState);
if (data && data.id) {
channel = await createChannel(channel, currentUserId)(dispatch, getState);
if (channel && channel.id) {
dispatch(setChannelDisplayName(displayName));
handleSelectChannel(data.id)(dispatch, getState);
handleSelectChannel(channel.id)(dispatch, getState);
}
};
}

View File

@@ -2,16 +2,16 @@
// See License.txt for license information.
import {addReaction} from 'mattermost-redux/actions/posts';
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts';
import {getPostsInCurrentChannel, makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts';
const getPostIdsForThread = makeGetPostIdsForThread();
const getPostsForThread = makeGetPostsForThread();
export function addReactionToLatestPost(emoji, rootId) {
return async (dispatch, getState) => {
const state = getState();
const postIds = rootId ? getPostIdsForThread(state, rootId) : getPostIdsInCurrentChannel(state);
const lastPostId = postIds[0];
const posts = rootId ? getPostsForThread(state, {rootId}) : getPostsInCurrentChannel(state);
const lastPost = posts[0];
dispatch(addReaction(lastPostId, emoji));
dispatch(addReaction(lastPost.id, emoji));
};
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';
export function initialize() {
return async (dispatch, getState) => {
setTimeout(() => {
dispatch({
type: ViewTypes.APPLICATION_INITIALIZED
}, getState);
}, 400);
};
}

View File

@@ -1,16 +1,16 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {General} from 'mattermost-redux/constants';
import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general';
import {getPosts} from 'mattermost-redux/actions/posts';
import {getMyTeams, getMyTeamMembers, selectTeam} from 'mattermost-redux/actions/teams';
import {ViewTypes} from 'app/constants';
import {
handleSelectChannel,
setChannelDisplayName,
retryGetPostsAction
setChannelDisplayName
} from 'app/actions/views/channel';
import {handleTeamChange, selectFirstAvailableTeam} from 'app/actions/views/select_team';
import {General} from 'mattermost-redux/constants';
import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general';
import {getChannelAndMyMember, markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
export function loadConfigAndLicense() {
return async (dispatch, getState) => {
@@ -23,39 +23,48 @@ export function loadConfigAndLicense() {
};
}
export function loadFromPushNotification(notification) {
export function queueNotification(notification) {
return async (dispatch, getState) => {
dispatch({type: ViewTypes.NOTIFICATION_CHANGED, data: notification}, getState);
};
}
export function clearNotification() {
return async (dispatch, getState) => {
dispatch({type: ViewTypes.NOTIFICATION_CHANGED, data: null}, getState);
};
}
export function goToNotification(notification) {
return async (dispatch, getState) => {
const state = getState();
const {data} = notification;
const {currentTeamId, teams, myMembers: myTeamMembers} = state.entities.teams;
const {currentChannelId} = state.entities.channels;
const {currentTeamId, teams} = state.entities.teams;
const {channels, currentChannelId, myMembers} = state.entities.channels;
const channelId = data.channel_id;
// when the notification does not have a team id is because its from a DM or GM
// if the notification does not have a team id is because its from a DM or GM
const teamId = data.team_id || currentTeamId;
//verify that we have the team loaded
if (teamId && (!teams[teamId] || !myTeamMembers[teamId])) {
await Promise.all([
getMyTeams()(dispatch, getState),
getMyTeamMembers()(dispatch, getState)
]);
dispatch(setChannelDisplayName(''));
if (teamId && teamId !== currentTeamId) {
handleTeamChange(teams[teamId], false)(dispatch, getState);
} else if (!teamId) {
await selectFirstAvailableTeam()(dispatch, getState);
}
// when the notification is from a team other than the current team
if (teamId !== currentTeamId) {
selectTeam({id: teamId})(dispatch, getState);
if (!channels[channelId] || !myMembers[channelId]) {
getChannelAndMyMember(channelId)(dispatch, getState);
}
// when the notification is from the same channel as the current channel
// we should get the posts
if (channelId === currentChannelId) {
await retryGetPostsAction(getPosts(channelId), dispatch, getState);
} else {
// when the notification is from a channel other than the current channel
dispatch(setChannelDisplayName(''));
if (channelId !== currentChannelId) {
handleSelectChannel(channelId)(dispatch, getState);
}
viewChannel(channelId)(dispatch, getState);
markChannelAsRead(channelId, currentChannelId)(dispatch, getState);
};
}
@@ -65,6 +74,8 @@ export function purgeOfflineStore() {
export default {
loadConfigAndLicense,
loadFromPushNotification,
queueNotification,
clearNotification,
goToNotification,
purgeOfflineStore
};

View File

@@ -12,23 +12,23 @@ import {NavigationTypes} from 'app/constants';
import {setChannelDisplayName} from './channel';
export function handleTeamChange(teamId, selectChannel = true) {
export function handleTeamChange(team, selectChannel = true) {
return async (dispatch, getState) => {
const state = getState();
const {currentTeamId} = state.entities.teams;
if (currentTeamId === teamId) {
if (currentTeamId === team.id) {
return;
}
const actions = [
setChannelDisplayName(''),
{type: TeamTypes.SELECT_TEAM, data: teamId}
{type: TeamTypes.SELECT_TEAM, data: team.id}
];
if (selectChannel) {
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: ''});
const lastChannelId = state.views.team.lastChannelForTeam[teamId] || '';
const lastChannelId = state.views.team.lastChannelForTeam[team.id] || '';
const currentChannelId = getCurrentChannelId(state);
viewChannel(lastChannelId, currentChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId, currentChannelId)(dispatch, getState);
@@ -45,7 +45,7 @@ export function selectFirstAvailableTeam() {
const firstTeam = Object.values(teams).sort((a, b) => a.display_name.localeCompare(b.display_name))[0];
if (firstTeam) {
handleTeamChange(firstTeam.id)(dispatch, getState);
handleTeamChange(firstTeam)(dispatch, getState);
} else {
EventEmitter.emit(NavigationTypes.NAVIGATION_NO_TEAMS);
}

View File

@@ -12,11 +12,3 @@ export function handleCommentDraftChanged(rootId, draft) {
}, getState);
};
}
export function handleCommentDraftSelectionChanged(rootId, cursorPosition) {
return {
type: ViewTypes.COMMENT_DRAFT_SELECTION_CHANGED,
rootId,
cursorPosition
};
}

View File

@@ -5,7 +5,7 @@ import {connect} from 'react-redux';
import {getUsersByUsername} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import AtMention from './at_mention';

View File

@@ -14,7 +14,7 @@ import {
filterMembersInCurrentTeam,
getMatchTermForAtMention
} from 'app/selectors/autocomplete';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import AtMention from './at_mention';

View File

@@ -5,7 +5,7 @@ import {connect} from 'react-redux';
import {getUser} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import AtMentionItem from './at_mention_item';

View File

@@ -15,7 +15,7 @@ import {
filterPrivateChannels,
getMatchTermForChannelMention
} from 'app/selectors/autocomplete';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelMention from './channel_mention';

View File

@@ -5,7 +5,7 @@ import {connect} from 'react-redux';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelMentionItem from './channel_mention_item';

View File

@@ -8,7 +8,7 @@ import {bindActionCreators} from 'redux';
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
import {addReactionToLatestPost} from 'app/actions/views/emoji';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import {EmojiIndicesByAlias} from 'app/utils/emojis';
import EmojiSuggestion from './emoji_suggestion';

View File

@@ -118,10 +118,6 @@ export default class Badge extends PureComponent {
};
render() {
if (!this.props.count) {
return null;
}
return (
<TouchableWithoutFeedback
{...this.panResponder.panHandlers}

View File

@@ -1,13 +1,12 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component} from 'react';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
BackHandler,
InteractionManager,
Keyboard,
Platform,
StyleSheet,
View
} from 'react-native';
@@ -25,7 +24,7 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
const DRAWER_INITIAL_OFFSET = 40;
const DRAWER_LANDSCAPE_OFFSET = 150;
export default class ChannelDrawer extends Component {
export default class ChannelDrawer extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
getTeams: PropTypes.func.isRequired,
@@ -38,6 +37,7 @@ export default class ChannelDrawer extends Component {
}).isRequired,
blurPostTextBox: PropTypes.func.isRequired,
children: PropTypes.node,
currentChannelId: PropTypes.string.isRequired,
currentTeamId: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
isLandscape: PropTypes.bool.isRequired,
@@ -60,6 +60,7 @@ export default class ChannelDrawer extends Component {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.state = {
openDrawer: false,
openDrawerOffset
};
}
@@ -73,14 +74,15 @@ export default class ChannelDrawer extends Component {
EventEmitter.on('close_channel_drawer', this.closeChannelDrawer);
EventEmitter.on(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
this.mounted = true;
}
componentWillReceiveProps(nextProps) {
const {isLandscape} = this.props;
if (nextProps.isLandscape !== isLandscape) {
const {isLandscape, isTablet} = this.props;
if (nextProps.isLandscape !== isLandscape || nextProps.isTablet || isTablet) {
if (this.state.openDrawerOffset !== 0) {
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (nextProps.isLandscape || this.props.isTablet) {
if (nextProps.isLandscape || nextProps.isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.setState({openDrawerOffset});
@@ -88,29 +90,17 @@ export default class ChannelDrawer extends Component {
}
}
shouldComponentUpdate(nextProps, nextState) {
const {currentTeamId, isLandscape, teamsCount} = this.props;
const {openDrawerOffset} = this.state;
if (nextState.openDrawerOffset !== openDrawerOffset) {
return true;
}
return nextProps.currentTeamId !== currentTeamId ||
nextProps.isLandscape !== isLandscape ||
nextProps.teamsCount !== teamsCount;
}
componentWillUnmount() {
EventEmitter.off('open_channel_drawer', this.openChannelDrawer);
EventEmitter.off('close_channel_drawer', this.closeChannelDrawer);
EventEmitter.off(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
this.mounted = false;
}
handleAndroidBack = () => {
if (this.refs.drawer && this.refs.drawer.isOpened()) {
this.refs.drawer.close();
if (this.state.openDrawer) {
this.setState({openDrawer: false});
return true;
}
@@ -118,8 +108,8 @@ export default class ChannelDrawer extends Component {
};
closeChannelDrawer = () => {
if (this.refs.drawer && this.refs.drawer.isOpened()) {
this.refs.drawer.close();
if (this.mounted) {
this.setState({openDrawer: false});
}
};
@@ -134,6 +124,13 @@ export default class ChannelDrawer extends Component {
InteractionManager.clearInteractionHandle(this.closeLeftHandle);
this.closeLeftHandle = null;
}
if (this.state.openDrawer && this.mounted) {
// The state doesn't get updated if you swipe to close
this.setState({
openDrawer: false
});
}
};
handleDrawerCloseStart = () => {
@@ -157,6 +154,13 @@ export default class ChannelDrawer extends Component {
if (!this.openLeftHandle) {
this.openLeftHandle = InteractionManager.createInteractionHandle();
}
if (!this.state.openDrawer && this.mounted) {
// The state doesn't get updated if you swipe to open
this.setState({
openDrawer: true
});
}
};
handleDrawerTween = (ratio) => {
@@ -188,14 +192,17 @@ export default class ChannelDrawer extends Component {
openChannelDrawer = () => {
this.props.blurPostTextBox();
if (this.refs.drawer && !this.refs.drawer.isOpened()) {
this.refs.drawer.open();
if (this.mounted) {
this.setState({
openDrawer: true
});
}
};
selectChannel = (channel, currentChannelId) => {
selectChannel = (channel) => {
const {
actions
actions,
currentChannelId
} = this.props;
const {
@@ -206,24 +213,21 @@ export default class ChannelDrawer extends Component {
viewChannel
} = actions;
setChannelLoading(channel.id !== currentChannelId);
setChannelLoading();
setChannelDisplayName(channel.display_name);
this.closeChannelDrawer();
InteractionManager.runAfterInteractions(() => {
handleSelectChannel(channel.id);
requestAnimationFrame(() => {
// mark the channel as viewed after all the frame has flushed
markChannelAsRead(channel.id, currentChannelId);
if (channel.id !== currentChannelId) {
viewChannel(currentChannelId);
}
});
markChannelAsRead(channel.id, currentChannelId);
if (channel.id !== currentChannelId) {
viewChannel(currentChannelId);
}
});
};
joinChannel = async (channel, currentChannelId) => {
joinChannel = async (channel) => {
const {
actions,
currentTeamId,
@@ -265,7 +269,7 @@ export default class ChannelDrawer extends Component {
return;
}
this.selectChannel(result.data, currentChannelId);
this.selectChannel(result.data);
};
onPageSelected = (index) => {
@@ -275,11 +279,7 @@ export default class ChannelDrawer extends Component {
onSearchEnds = () => {
//hack to update the drawer when the offset changes
const {isLandscape, isTablet} = this.props;
if (this.refs.drawer) {
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
}
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (isLandscape || isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
@@ -288,21 +288,18 @@ export default class ChannelDrawer extends Component {
};
onSearchStart = () => {
if (this.refs.drawer) {
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
}
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
this.setState({openDrawerOffset: 0});
};
showTeams = () => {
if (this.drawerSwiper && this.swiperIndex === 1 && this.props.teamsCount > 1) {
if (this.swiperIndex === 1 && this.props.teamsCount > 1) {
this.drawerSwiper.getWrappedInstance().showTeamsPage();
}
};
resetDrawer = () => {
if (this.drawerSwiper && this.swiperIndex !== 1) {
if (this.swiperIndex !== 1) {
this.drawerSwiper.getWrappedInstance().resetPage();
}
};
@@ -356,7 +353,6 @@ export default class ChannelDrawer extends Component {
onShowTeams={this.showTeams}
onSearchStart={this.onSearchStart}
onSearchEnds={this.onSearchEnds}
theme={theme}
/>
</View>
);
@@ -367,6 +363,7 @@ export default class ChannelDrawer extends Component {
onPageSelected={this.onPageSelected}
openDrawerOffset={openDrawerOffset}
showTeams={showTeams}
theme={theme}
>
{lists}
</DrawerSwiper>
@@ -375,11 +372,12 @@ export default class ChannelDrawer extends Component {
render() {
const {children} = this.props;
const {openDrawerOffset} = this.state;
const {openDrawer, openDrawerOffset} = this.state;
return (
<Drawer
ref='drawer'
open={openDrawer}
onOpenStart={this.handleDrawerOpenStart}
onOpen={this.handleDrawerOpen}
onClose={this.handleDrawerClose}
@@ -402,8 +400,6 @@ export default class ChannelDrawer extends Component {
tweenDuration={100}
tweenHandler={this.handleDrawerTween}
elevation={-5}
bottomPanOffset={Platform.OS === 'ios' ? 46 : 64}
topPanOffset={Platform.OS === 'ios' ? 64 : 46}
styles={{
main: {
shadowColor: '#000000',

View File

@@ -10,63 +10,45 @@ import {
} from 'react-native';
import Badge from 'app/components/badge';
import ChannelIcon from 'app/components/channel_icon';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import ChanneIcon from 'app/components/channel_icon';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ChannelItem extends PureComponent {
static propTypes = {
channelId: PropTypes.string.isRequired,
currentChannelId: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
fake: PropTypes.bool,
isUnread: PropTypes.bool,
mentions: PropTypes.number.isRequired,
channel: PropTypes.object.isRequired,
onSelectChannel: PropTypes.func.isRequired,
status: PropTypes.string,
type: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired,
hasUnread: PropTypes.bool.isRequired,
mentions: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired
};
onPress = wrapWithPreventDoubleTap(() => {
const {channelId, currentChannelId, displayName, fake, onSelectChannel, type} = this.props;
onPress = () => {
const {channel, onSelectChannel} = this.props;
requestAnimationFrame(() => {
onSelectChannel({id: channelId, display_name: displayName, fake, type}, currentChannelId);
preventDoubleTap(onSelectChannel, this, channel);
});
});
};
render() {
const {
channelId,
currentChannelId,
displayName,
isUnread,
mentions,
status,
channel,
theme,
type
mentions,
hasUnread,
isActive
} = this.props;
const style = getStyleSheet(theme);
const isActive = channelId === currentChannelId;
let extraItemStyle;
let extraTextStyle;
let extraBorder;
if (isActive) {
extraItemStyle = style.itemActive;
extraTextStyle = style.textActive;
extraBorder = (
<View style={style.borderActive}/>
);
} else if (isUnread) {
extraTextStyle = style.textUnread;
}
let activeItem;
let activeText;
let unreadText;
let activeBorder;
let badge;
if (mentions) {
if (mentions && !isActive) {
badge = (
<Badge
style={style.badge}
@@ -79,16 +61,28 @@ export default class ChannelItem extends PureComponent {
);
}
if (hasUnread) {
unreadText = style.textUnread;
}
if (isActive) {
activeItem = style.itemActive;
activeText = style.textActive;
activeBorder = (
<View style={style.borderActive}/>
);
}
const icon = (
<ChannelIcon
<ChanneIcon
isActive={isActive}
channelId={channelId}
isUnread={isUnread}
membersCount={displayName.split(',').length}
hasUnread={hasUnread}
membersCount={channel.display_name.split(',').length}
size={16}
status={status}
status={channel.status}
theme={theme}
type={type}
type={channel.type}
/>
);
@@ -98,15 +92,15 @@ export default class ChannelItem extends PureComponent {
onPress={this.onPress}
>
<View style={style.container}>
{extraBorder}
<View style={[style.item, extraItemStyle]}>
{activeBorder}
<View style={[style.item, activeItem]}>
{icon}
<Text
style={[style.text, extraTextStyle]}
style={[style.text, unreadText, activeText]}
ellipsizeMode='tail'
numberOfLines={1}
>
{displayName}
{channel.display_name}
</Text>
{badge}
</View>

View File

@@ -1,33 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getCurrentChannelId, makeGetChannel, getMyChannelMember} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ChannelItem from './channel_item';
function makeMapStateToProps() {
const getChannel = makeGetChannel();
return (state, ownProps) => {
const channel = ownProps.channel || getChannel(state, {id: ownProps.channelId});
let member;
if (ownProps.isUnread) {
member = getMyChannelMember(state, ownProps.channelId);
}
return {
currentChannelId: getCurrentChannelId(state),
displayName: channel.display_name,
fake: channel.fake,
mentions: member ? member.mention_count : 0,
status: channel.status,
theme: getTheme(state),
type: channel.type
};
};
}
export default connect(makeMapStateToProps)(ChannelItem);

View File

@@ -18,7 +18,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import FilteredList from './filtered_list';
import List from './list';
import SwitchTeamsButton from './switch_teams_button';
import SwitchTeams from './switch_teams';
class ChannelsList extends React.PureComponent {
static propTypes = {
@@ -45,16 +45,14 @@ class ChannelsList extends React.PureComponent {
});
}
onSelectChannel = (channel, currentChannelId) => {
onSelectChannel = (channel) => {
if (channel.fake) {
this.props.onJoinChannel(channel, currentChannelId);
this.props.onJoinChannel(channel);
} else {
this.props.onSelectChannel(channel, currentChannelId);
this.props.onSelectChannel(channel);
}
if (this.refs.search_bar) {
this.refs.search_bar.cancel();
}
this.refs.search_bar.cancel();
};
openSettingsModal = wrapWithPreventDoubleTap(() => {
@@ -144,18 +142,17 @@ class ChannelsList extends React.PureComponent {
<View style={styles.searchContainer}>
<SearchBar
ref='search_bar'
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Jump to...'})}
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Jump to a conversation'})}
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={{
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.2),
color: theme.sidebarHeaderTextColor,
fontSize: 15,
lineHeight: 66
fontSize: 13
}}
placeholderTextColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.8)}
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
titleCancelColor={theme.sidebarHeaderTextColor}
selectionColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
@@ -174,12 +171,10 @@ class ChannelsList extends React.PureComponent {
>
<View style={styles.statusBar}>
<View style={styles.headerContainer}>
<View style={styles.switchContainer}>
<SwitchTeamsButton
searching={searching}
onShowTeams={onShowTeams}
/>
</View>
<SwitchTeams
searching={searching}
showTeams={onShowTeams}
/>
{title}
{settings}
</View>
@@ -227,10 +222,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
fontWeight: 'normal',
paddingLeft: 16
},
switchContainer: {
position: 'relative',
top: -1
},
settingsContainer: {
alignItems: 'center',
justifyContent: 'center',
@@ -297,6 +288,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
above: {
backgroundColor: theme.mentionBj,
top: 9
},
indicatorText: {
backgroundColor: 'transparent',
color: theme.mentionColor,
fontSize: 14,
paddingVertical: 2,
paddingHorizontal: 4,
textAlign: 'center',
textAlignVertical: 'center'
}
};
});

View File

@@ -95,25 +95,25 @@ class FilteredList extends Component {
}
onSelectChannel = (channel) => {
const {actions, currentChannel} = this.props;
const {makeGroupMessageVisibleIfNecessary} = actions;
const {makeGroupMessageVisibleIfNecessary} = this.props.actions;
if (channel.type === General.GM_CHANNEL) {
makeGroupMessageVisibleIfNecessary(channel.id);
}
this.props.onSelectChannel(channel, currentChannel.id);
this.props.onSelectChannel(channel);
};
createChannelElement = (channel) => {
return (
<ChannelDrawerItem
ref={channel.id}
channelId={channel.id}
channel={channel}
isUnread={false}
hasUnread={false}
mentions={0}
onSelectChannel={this.onSelectChannel}
isActive={channel.isCurrent || false}
theme={this.props.theme}
/>
);
};

View File

@@ -3,12 +3,13 @@
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelsList from './channels_list';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
return {
...ownProps,
theme: getTheme(state)
};
}

View File

@@ -4,48 +4,26 @@
import {connect} from 'react-redux';
import {General} from 'mattermost-redux/constants';
import {
getSortedUnreadChannelIds,
getSortedFavoriteChannelIds,
getSortedPublicChannelIds,
getSortedPrivateChannelIds,
getSortedDirectChannelIds
} from 'mattermost-redux/selectors/entities/channels';
import {getChannelsWithUnreadSection, getCurrentChannel, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {getTheme, getFavoritesPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {showCreateOption} from 'mattermost-redux/utils/channel_utils';
import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
import List from './list';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {config, license} = state.entities.general;
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
const unreadChannelIds = getSortedUnreadChannelIds(state);
const favoriteChannelIds = getSortedFavoriteChannelIds(state);
const publicChannelIds = getSortedPublicChannelIds(state);
const privateChannelIds = getSortedPrivateChannelIds(state);
const directChannelIds = getSortedDirectChannelIds(state);
return {
canCreatePrivateChannels: showCreateOption(config, license, General.PRIVATE_CHANNEL, isAdmin(roles), isSystemAdmin(roles)),
unreadChannelIds,
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
directChannelIds,
theme: getTheme(state)
channelMembers: getMyChannelMemberships(state),
channels: getChannelsWithUnreadSection(state),
currentChannel: getCurrentChannel(state),
theme: getTheme(state),
...ownProps
};
}
function areStatesEqual(next, prev) {
const equalRoles = getCurrentUserRoles(prev) === getCurrentUserRoles(next);
const equalChannels = next.entities.channels === prev.entities.channels;
const equalConfig = next.entities.general.config === prev.entities.general.config;
const equalUsers = next.entities.users.profiles === prev.entities.users.profiles;
const equalFav = getFavoritesPreferences(next) === getFavoritesPreferences(prev);
return equalChannels && equalConfig && equalRoles && equalUsers && equalFav;
}
export default connect(mapStateToProps, null, null, {pure: true, areStatesEqual})(List);
export default connect(mapStateToProps, null)(List);

View File

@@ -1,11 +1,11 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import deepEqual from 'deep-equal';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
InteractionManager,
SectionList,
FlatList,
Text,
TouchableHighlight,
View
@@ -13,36 +13,38 @@ import {
import {injectIntl, intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {General} from 'mattermost-redux/constants';
import {debounce} from 'mattermost-redux/actions/helpers';
import ChannelItem from 'app/components/channel_drawer/channels_list/channel_item';
import UnreadIndicator from 'app/components/channel_drawer/channels_list/unread_indicator';
import FormattedText from 'app/components/formatted_text';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity} from 'app/utils/theme';
class List extends PureComponent {
import {General} from 'mattermost-redux/constants';
import ChannelItem from 'app/components/channel_drawer/channels_list/channel_item';
import UnreadIndicator from 'app/components/channel_drawer/channels_list/unread_indicator';
class List extends Component {
static propTypes = {
canCreatePrivateChannels: PropTypes.bool.isRequired,
directChannelIds: PropTypes.array.isRequired,
favoriteChannelIds: PropTypes.array.isRequired,
channels: PropTypes.object.isRequired,
channelMembers: PropTypes.object,
currentChannel: PropTypes.object,
intl: intlShape.isRequired,
navigator: PropTypes.object,
onSelectChannel: PropTypes.func.isRequired,
publicChannelIds: PropTypes.array.isRequired,
privateChannelIds: PropTypes.array.isRequired,
styles: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
unreadChannelIds: PropTypes.array.isRequired
theme: PropTypes.object.isRequired
};
static defaultProps = {
currentChannel: {}
};
constructor(props) {
super(props);
this.firstUnreadChannel = null;
this.state = {
sections: this.buildSections(props),
showIndicator: false,
width: 0
dataSource: this.buildData(props),
showAbove: false
};
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
@@ -50,96 +52,106 @@ class List extends PureComponent {
});
}
shouldComponentUpdate(nextProps, nextState) {
return !deepEqual(this.props, nextProps, {strict: true}) || !deepEqual(this.state, nextState, {strict: true});
}
componentWillReceiveProps(nextProps) {
const {
canCreatePrivateChannels,
directChannelIds,
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
unreadChannelIds
} = this.props;
if (nextProps.canCreatePrivateChannels !== canCreatePrivateChannels ||
nextProps.directChannelIds !== directChannelIds || nextProps.favoriteChannelIds !== favoriteChannelIds ||
nextProps.publicChannelIds !== publicChannelIds || nextProps.privateChannelIds !== privateChannelIds ||
nextProps.unreadChannelIds !== unreadChannelIds) {
this.setState({sections: this.buildSections(nextProps)});
}
this.setState({
dataSource: this.buildData(nextProps)
}, () => {
if (this.refs.list) {
this.refs.list.recordInteraction();
this.updateUnreadIndicators({
viewableItems: Array.from(this.refs.list._listRef._viewabilityHelper._viewableItems.values()) //eslint-disable-line
});
}
});
}
componentDidUpdate(prevProps, prevState) {
if (prevState.sections !== this.state.sections && this.refs.list) {
this.refs.list.recordInteraction();
this.updateUnreadIndicators({
viewableItems: Array.from(this.refs.list._wrapperListRef._listRef._viewabilityHelper._viewableItems.values()) //eslint-disable-line
updateUnreadIndicators = ({viewableItems}) => {
let showAbove = false;
const visibleIndexes = viewableItems.map((v) => v.index);
if (visibleIndexes.length) {
const {dataSource} = this.state;
const firstVisible = parseInt(visibleIndexes[0], 10);
if (this.firstUnreadChannel) {
const index = dataSource.findIndex((item) => {
return item.display_name === this.firstUnreadChannel;
});
showAbove = index < firstVisible;
}
this.setState({
showAbove
});
}
}
buildSections = (props) => {
const {
canCreatePrivateChannels,
directChannelIds,
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
unreadChannelIds
} = props;
const sections = [];
if (unreadChannelIds.length) {
sections.push({
id: 'mobile.channel_list.unreads',
defaultMessage: 'UNREADS',
data: unreadChannelIds,
renderItem: this.renderUnreadItem,
topSeparator: false,
bottomSeparator: true
});
}
if (favoriteChannelIds.length) {
sections.push({
id: 'sidebar.favorite',
defaultMessage: 'FAVORITES',
data: favoriteChannelIds,
topSeparator: unreadChannelIds.length > 0,
bottomSeparator: true
});
}
sections.push({
action: this.goToMoreChannels,
id: 'sidebar.channels',
defaultMessage: 'PUBLIC CHANNELS',
data: publicChannelIds,
topSeparator: favoriteChannelIds.length > 0 || unreadChannelIds.length > 0,
bottomSeparator: publicChannelIds.length > 0
});
sections.push({
action: canCreatePrivateChannels ? this.goToCreatePrivateChannel : null,
id: 'sidebar.pg',
defaultMessage: 'PRIVATE CHANNELS',
data: privateChannelIds,
topSeparator: true,
bottomSeparator: privateChannelIds.length > 0
});
sections.push({
action: this.goToDirectMessages,
id: 'sidebar.direct',
defaultMessage: 'DIRECT MESSAGES',
data: directChannelIds,
topSeparator: true,
bottomSeparator: directChannelIds.length > 0
});
return sections;
};
goToCreatePrivateChannel = wrapWithPreventDoubleTap(() => {
onSelectChannel = (channel) => {
this.props.onSelectChannel(channel);
};
onLayout = (event) => {
const {width} = event.nativeEvent.layout;
this.width = width;
};
getUnreadMessages = (channel) => {
const member = this.props.channelMembers[channel.id];
let mentions = 0;
let unreadCount = 0;
if (member && channel) {
mentions = member.mention_count;
unreadCount = channel.total_msg_count - member.msg_count;
if (member.notify_props && member.notify_props.mark_unread === General.MENTION) {
unreadCount = 0;
}
}
return {
mentions,
unreadCount
};
};
findUnreadChannels = (data) => {
data.forEach((c) => {
if (c.id) {
const {mentions, unreadCount} = this.getUnreadMessages(c);
const unread = (mentions + unreadCount) > 0;
if (unread && c.id !== this.props.currentChannel.id) {
if (!this.firstUnreadChannel) {
this.firstUnreadChannel = c.display_name;
}
}
}
});
};
createChannelElement = (channel) => {
const {mentions, unreadCount} = this.getUnreadMessages(channel);
const msgCount = mentions + unreadCount;
const unread = msgCount > 0;
return (
<ChannelItem
ref={channel.id}
channel={channel}
hasUnread={unread}
mentions={mentions}
onSelectChannel={this.onSelectChannel}
isActive={channel.isCurrent}
theme={this.props.theme}
/>
);
};
createPrivateChannel = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
@@ -161,7 +173,75 @@ class List extends PureComponent {
});
});
goToDirectMessages = wrapWithPreventDoubleTap(() => {
buildChannels = (props) => {
const {canCreatePrivateChannels, styles} = props;
const {
unreadChannels,
favoriteChannels,
publicChannels,
privateChannels,
directAndGroupChannels
} = props.channels;
const data = [];
if (unreadChannels.length) {
data.push(
this.renderTitle(styles, 'mobile.channel_list.unreads', 'UNREADS', null, false, true),
...unreadChannels
);
}
if (favoriteChannels.length) {
data.push(
this.renderTitle(styles, 'sidebar.favorite', 'FAVORITES', null, unreadChannels.length > 0, true),
...favoriteChannels
);
}
data.push(
this.renderTitle(styles, 'sidebar.channels', 'CHANNELS', this.showMoreChannelsModal, favoriteChannels.length > 0, publicChannels.length > 0),
...publicChannels
);
let createPrivateChannel;
if (canCreatePrivateChannels) {
createPrivateChannel = this.createPrivateChannel;
}
data.push(
this.renderTitle(styles, 'sidebar.pg', 'PRIVATE CHANNELS', createPrivateChannel, true, privateChannels.length > 0),
...privateChannels
);
data.push(
this.renderTitle(styles, 'sidebar.direct', 'DIRECT MESSAGES', this.showDirectMessagesModal, true, directAndGroupChannels.length > 0),
...directAndGroupChannels
);
return data;
};
buildData = (props) => {
if (!props.currentChannel) {
return null;
}
const data = this.buildChannels(props);
this.firstUnreadChannel = null;
this.findUnreadChannels(data);
return data;
};
scrollToTop = () => {
this.refs.list.scrollToOffset({
x: 0,
y: 0,
animated: true
});
}
showDirectMessagesModal = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
@@ -185,7 +265,7 @@ class List extends PureComponent {
});
});
goToMoreChannels = wrapWithPreventDoubleTap(() => {
showMoreChannelsModal = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
@@ -206,18 +286,6 @@ class List extends PureComponent {
});
});
keyExtractor = (item) => item.id || item;
onSelectChannel = (channel, currentChannelId) => {
const {onSelectChannel} = this.props;
onSelectChannel(channel, currentChannelId);
};
onLayout = (event) => {
const {width} = event.nativeEvent.layout;
this.setState({width: width - 40});
};
renderSectionAction = (styles, action) => {
const {theme} = this.props;
return (
@@ -234,114 +302,83 @@ class List extends PureComponent {
);
};
renderSectionSeparator = () => {
const {styles} = this.props;
renderDivider = (styles, marginLeft) => {
return (
<View style={[styles.divider]}/>
<View
style={[styles.divider, {marginLeft}]}
/>
);
};
renderItem = ({item}) => {
return (
<ChannelItem
channelId={item}
onSelectChannel={this.onSelectChannel}
/>
);
};
renderUnreadItem = ({item}) => {
return (
<ChannelItem
channelId={item}
isUnread={true}
onSelectChannel={this.onSelectChannel}
/>
);
};
renderSectionHeader = ({section}) => {
const {intl, styles} = this.props;
const {
action,
bottomSeparator,
defaultMessage,
id,
topSeparator
} = section;
return (
<View>
{topSeparator && this.renderSectionSeparator()}
<View style={styles.titleContainer}>
<Text style={styles.title}>
{intl.formatMessage({id, defaultMessage}).toUpperCase()}
</Text>
{action && this.renderSectionAction(styles, action)}
</View>
{bottomSeparator && this.renderSectionSeparator()}
</View>
);
};
scrollToTop = () => {
if (this.refs.list) {
this.refs.list._wrapperListRef._listRef.scrollToOffset({ //eslint-disable-line no-underscore-dangle
x: 0,
y: 0,
animated: true
});
if (!item.isTitle) {
return this.createChannelElement(item);
}
return item.title;
};
emitUnreadIndicatorChange = debounce((showIndicator) => {
this.setState({showIndicator});
}, 100);
renderTitle = (styles, id, defaultMessage, action, topDivider, bottomDivider) => {
const {formatMessage} = this.props.intl;
updateUnreadIndicators = ({viewableItems}) => {
InteractionManager.runAfterInteractions(() => {
const {unreadChannelIds} = this.props;
const firstUnread = unreadChannelIds.length && unreadChannelIds[0];
if (firstUnread && viewableItems.length) {
const isVisible = viewableItems.find((v) => v.item === firstUnread);
return this.emitUnreadIndicatorChange(!isVisible);
}
return this.emitUnreadIndicatorChange(false);
});
return {
id,
isTitle: true,
title: (
<View>
{topDivider && this.renderDivider(styles, 0)}
<View style={styles.titleContainer}>
<Text style={styles.title}>
{formatMessage({id, defaultMessage}).toUpperCase()}
</Text>
{action && this.renderSectionAction(styles, action)}
</View>
{bottomDivider && this.renderDivider(styles, 0)}
</View>
)
};
};
render() {
const {styles, theme} = this.props;
const {sections, width, showIndicator} = this.state;
const {styles} = this.props;
const {dataSource, showAbove} = this.state;
let above;
if (showAbove) {
above = (
<UnreadIndicator
style={[styles.above, {width: (this.width - 40)}]}
onPress={this.scrollToTop}
text={(
<FormattedText
style={styles.indicatorText}
id='sidebar.unreadAbove'
defaultMessage='Unread post(s) above'
/>
)}
/>
);
}
return (
<View
style={styles.container}
onLayout={this.onLayout}
>
<SectionList
<FlatList
ref='list'
sections={sections}
data={dataSource}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
keyExtractor={this.keyExtractor}
keyExtractor={(item) => item.id}
onViewableItemsChanged={this.updateUnreadIndicators}
keyboardDismissMode='on-drag'
maxToRenderPerBatch={10}
stickySectionHeadersEnabled={false}
viewabilityConfig={{
viewAreaCoveragePercentThreshold: 3,
waitForInteraction: true
waitForInteraction: false
}}
/>
<UnreadIndicator
show={showIndicator}
style={[styles.above, {width}]}
onPress={this.scrollToTop}
theme={theme}
/>
{above}
</View>
);
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import SwitchTeams from './switch_teams';
function mapStateToProps(state, ownProps) {
return {
currentTeam: getCurrentTeam(state),
teamMembers: getTeamMemberships(state),
theme: getTheme(state),
...ownProps
};
}
export default connect(mapStateToProps)(SwitchTeams);

View File

@@ -14,50 +14,96 @@ import Badge from 'app/components/badge';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class SwitchTeamsButton extends React.PureComponent {
export default class SwitchTeams extends React.PureComponent {
static propTypes = {
currentTeamId: PropTypes.string,
displayName: PropTypes.string,
currentTeam: PropTypes.object,
searching: PropTypes.bool.isRequired,
onShowTeams: PropTypes.func.isRequired,
mentionCount: PropTypes.number.isRequired,
teamsCount: PropTypes.number.isRequired,
showTeams: PropTypes.func.isRequired,
teamMembers: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
constructor(props) {
super(props);
this.state = {
badgeCount: this.getBadgeCount(props)
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.currentTeam !== this.props.currentTeam || nextProps.teamMembers !== this.props.teamMembers) {
this.setState({
badgeCount: this.getBadgeCount(nextProps)
});
}
}
getBadgeCount = (props) => {
const {
currentTeam,
teamMembers
} = props;
let mentionCount = 0;
let messageCount = 0;
Object.values(teamMembers).forEach((m) => {
if (m.team_id !== currentTeam.id) {
mentionCount = mentionCount + (m.mention_count || 0);
messageCount = messageCount + (m.msg_count || 0);
}
});
let badgeCount;
if (mentionCount) {
badgeCount = mentionCount;
} else if (messageCount) {
badgeCount = -1;
} else {
badgeCount = 0;
}
return badgeCount;
};
showTeams = wrapWithPreventDoubleTap(() => {
this.props.onShowTeams();
this.props.showTeams();
});
render() {
const {
currentTeamId,
displayName,
mentionCount,
currentTeam,
searching,
teamsCount,
teamMembers,
theme
} = this.props;
if (!currentTeamId) {
if (!currentTeam) {
return null;
}
if (searching || teamsCount < 2) {
const {
badgeCount
} = this.state;
if (searching || Object.keys(teamMembers).length < 2) {
return null;
}
const styles = getStyleSheet(theme);
const badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={mentionCount}
minHeight={20}
minWidth={20}
/>
);
let badge;
if (badgeCount) {
badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={badgeCount}
minHeight={20}
minWidth={20}
/>
);
}
return (
<View>
@@ -73,7 +119,7 @@ export default class SwitchTeamsButton extends React.PureComponent {
/>
<View style={styles.switcherDivider}/>
<Text style={styles.switcherTeam}>
{displayName.substr(0, 2).toUpperCase()}
{currentTeam.display_name.substr(0, 2).toUpperCase()}
</Text>
</View>
</TouchableHighlight>
@@ -93,7 +139,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
height: 32,
justifyContent: 'center',
marginLeft: 6,
marginRight: 5,
marginRight: 10,
paddingHorizontal: 6
},
switcherDivider: {

View File

@@ -1,23 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeam, getMyTeamsCount, getChannelDrawerBadgeCount} from 'mattermost-redux/selectors/entities/teams';
import SwitchTeamsButton from './switch_teams_button';
function mapStateToProps(state) {
const team = getCurrentTeam(state);
return {
currentTeamId: team.id,
displayName: team.display_name,
mentionCount: getChannelDrawerBadgeCount(state),
teamsCount: getMyTeamsCount(state),
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(SwitchTeamsButton);

View File

@@ -0,0 +1,49 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
Text,
TouchableWithoutFeedback,
View,
ViewPropTypes
} from 'react-native';
export default class UnreadIndicator extends PureComponent {
static propTypes = {
style: ViewPropTypes.style,
textStyle: Text.propTypes.style,
text: PropTypes.node.isRequired,
onPress: PropTypes.func
};
static defaultProps = {
onPress: () => true
};
render() {
return (
<TouchableWithoutFeedback onPress={this.props.onPress}>
<View
style={[Styles.container, this.props.style]}
>
{this.props.text}
</View>
</TouchableWithoutFeedback>
);
}
}
const Styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
position: 'absolute',
borderRadius: 15,
marginHorizontal: 15,
height: 25
}
});

View File

@@ -1,48 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {StyleSheet, View} from 'react-native';
import Svg, {
G,
Path
} from 'react-native-svg';
export default class AboveIcon extends PureComponent {
static propTypes = {
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
color: PropTypes.string.isRequired
};
render() {
const {color, height, width} = this.props;
return (
<View style={[style.container, {height, width}]}>
<Svg
width={width}
height={height}
viewBox='0 0 10 10'
>
<G transform='matrix(1,0,0,1,-20,-18)'>
<G transform='matrix(0.0330723,0,0,0.0322634,15.8132,12.3164)'>
<Path
d='M245.803,377.493C245.803,377.493 204.794,336.485 179.398,311.088C168.55,300.24 150.962,300.24 140.114,311.088C138.327,312.875 136.517,314.686 134.73,316.473C123.882,327.321 123.882,344.908 134.73,355.756C167.972,388.998 233.949,454.975 256.949,477.975C262.158,483.184 269.223,486.111 276.591,486.111C277.38,486.111 278.176,486.111 278.965,486.111C286.332,486.111 293.397,483.184 298.607,477.975C321.607,454.975 387.584,388.998 420.826,355.756C431.674,344.908 431.674,327.321 420.826,316.473C419.039,314.686 417.228,312.875 415.441,311.088C404.593,300.24 387.005,300.24 376.158,311.088C350.761,336.485 309.753,377.493 309.753,377.493C309.753,377.493 309.753,279.687 309.753,203.94C309.753,196.573 306.826,189.508 301.617,184.298C296.408,179.089 289.342,176.162 281.975,176.162C279.191,176.162 276.364,176.162 273.58,176.162C266.213,176.162 259.148,179.089 253.939,184.298C248.729,189.508 245.803,196.573 245.803,203.94L245.803,377.493Z'
fill={color}
/>
</G>
</G>
</Svg>
</View>
);
}
}
const style = StyleSheet.create({
container: {
alignItems: 'flex-start',
transform: [{rotate: '180deg'}]
}
});

View File

@@ -1,80 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
TouchableWithoutFeedback,
View,
ViewPropTypes
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import AboveIcon from './above_icon';
export default class UnreadIndicator extends PureComponent {
static propTypes = {
show: PropTypes.bool,
style: ViewPropTypes.style,
onPress: PropTypes.func,
theme: PropTypes.object.isRequired
};
static defaultProps = {
onPress: () => true
};
render() {
const {onPress, show, theme} = this.props;
if (!show) {
return null;
}
const style = getStyleSheet(theme);
return (
<TouchableWithoutFeedback onPress={onPress}>
<View
style={[style.container, this.props.style]}
>
<FormattedText
style={style.indicatorText}
id='sidebar.unreads'
defaultMessage='More unreads'
/>
<AboveIcon
width={12}
height={12}
color={theme.mentionColor}
/>
</View>
</TouchableWithoutFeedback>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
position: 'absolute',
borderRadius: 15,
marginHorizontal: 15,
height: 25
},
indicatorText: {
backgroundColor: 'transparent',
color: theme.mentionColor,
fontSize: 14,
paddingVertical: 2,
paddingHorizontal: 4,
textAlign: 'center',
textAlignVertical: 'center'
}
};
});

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component} from 'react';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {StyleSheet} from 'react-native';
@@ -9,7 +9,7 @@ import {changeOpacity} from 'app/utils/theme';
import Swiper from 'app/components/swiper';
export default class DrawerSwiper extends Component {
export default class DrawerSwiper extends PureComponent {
static propTypes = {
children: PropTypes.node.isRequired,
deviceWidth: PropTypes.number.isRequired,
@@ -24,28 +24,16 @@ export default class DrawerSwiper extends Component {
openDrawerOffset: 0
};
shouldComponentUpdate(nextProps) {
const {deviceWidth, showTeams, theme} = this.props;
return nextProps.deviceWidth !== deviceWidth ||
nextProps.showTeams !== showTeams || nextProps.theme !== theme;
}
runOnLayout = (shouldRun = true) => {
if (this.refs.swiper) {
this.refs.swiper.runOnLayout = shouldRun;
}
this.refs.swiper.runOnLayout = shouldRun;
};
resetPage = () => {
if (this.refs.swiper) {
this.refs.swiper.scrollToIndex(1, false);
}
this.refs.swiper.scrollToIndex(1, false);
};
scrollToStart = () => {
if (this.refs.swiper) {
this.refs.swiper.scrollToStart();
}
this.refs.swiper.scrollToStart();
};
swiperPageSelected = (index) => {
@@ -53,9 +41,7 @@ export default class DrawerSwiper extends Component {
};
showTeamsPage = () => {
if (this.refs.swiper) {
this.refs.swiper.scrollToIndex(0, true);
}
this.refs.swiper.scrollToIndex(0, true);
};
render() {

View File

@@ -3,16 +3,15 @@
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getDimensions} from 'app/selectors/device';
import {getDimensions, isLandscape} from 'app/selectors/device';
import DraweSwiper from './drawer_swiper';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
return {
...ownProps,
...getDimensions(state),
theme: getTheme(state)
isLandscape: isLandscape(state)
};
}

View File

@@ -6,24 +6,27 @@ import {connect} from 'react-redux';
import {joinChannel, viewChannel, markChannelAsRead} from 'mattermost-redux/actions/channels';
import {getTeams} from 'mattermost-redux/actions/teams';
import {getCurrentTeamId, getMyTeamsCount} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import {handleSelectChannel, setChannelDisplayName, setChannelLoading} from 'app/actions/views/channel';
import {makeDirectChannel} from 'app/actions/views/more_dms';
import {isLandscape, isTablet} from 'app/selectors/device';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelDrawer from './channel_drawer.js';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {currentUserId} = state.entities.users;
return {
...ownProps,
currentTeamId: getCurrentTeamId(state),
currentChannelId: getCurrentChannelId(state),
currentUserId,
isLandscape: isLandscape(state),
isTablet: isTablet(state),
teamsCount: getMyTeamsCount(state),
teamsCount: Object.keys(getTeamMemberships(state)).length,
theme: getTheme(state)
};
}

View File

@@ -5,24 +5,23 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getCurrentTeamId, getJoinableTeamIds, getMySortedTeamIds} from 'mattermost-redux/selectors/entities/teams';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId, getJoinableTeams} from 'mattermost-redux/selectors/entities/teams';
import {handleTeamChange} from 'app/actions/views/select_team';
import {getCurrentLocale} from 'app/selectors/i18n';
import {getTheme} from 'app/selectors/preferences';
import {getMySortedTeams} from 'app/selectors/teams';
import {removeProtocol} from 'app/utils/url';
import TeamsList from './teams_list';
function mapStateToProps(state) {
const locale = getCurrentLocale(state);
function mapStateToProps(state, ownProps) {
return {
canJoinOtherTeams: getJoinableTeamIds(state).length > 0,
joinableTeams: getJoinableTeams(state),
currentTeamId: getCurrentTeamId(state),
currentUrl: removeProtocol(getCurrentUrl(state)),
teamIds: getMySortedTeamIds(state, locale),
theme: getTheme(state)
teams: getMySortedTeams(state),
theme: getTheme(state),
...ownProps
};
}

View File

@@ -24,13 +24,13 @@ class TeamsList extends PureComponent {
actions: PropTypes.shape({
handleTeamChange: PropTypes.func.isRequired
}).isRequired,
canJoinOtherTeams: PropTypes.bool.isRequired,
closeChannelDrawer: PropTypes.func.isRequired,
currentTeamId: PropTypes.string.isRequired,
currentUrl: PropTypes.string.isRequired,
intl: intlShape.isRequired,
joinableTeams: PropTypes.object.isRequired,
navigator: PropTypes.object.isRequired,
teamIds: PropTypes.array.isRequired,
teams: PropTypes.array.isRequired,
theme: PropTypes.object.isRequired
};
@@ -42,11 +42,11 @@ class TeamsList extends PureComponent {
});
}
selectTeam = (teamId) => {
selectTeam = (team) => {
requestAnimationFrame(() => {
const {actions, closeChannelDrawer, currentTeamId} = this.props;
if (teamId !== currentTeamId) {
actions.handleTeamChange(teamId);
if (team.id !== currentTeamId) {
actions.handleTeamChange(team);
}
closeChannelDrawer();
@@ -81,25 +81,25 @@ class TeamsList extends PureComponent {
});
});
keyExtractor = (item) => {
return item;
};
keyExtractor = (team) => {
return team.id;
}
renderItem = ({item}) => {
return (
<TeamsListItem
selectTeam={this.selectTeam}
teamId={item}
team={item}
/>
);
};
render() {
const {canJoinOtherTeams, teamIds, theme} = this.props;
const {joinableTeams, teams, theme} = this.props;
const styles = getStyleSheet(theme);
let moreAction;
if (canJoinOtherTeams) {
if (Object.keys(joinableTeams).length) {
moreAction = (
<TouchableHighlight
style={styles.moreActionContainer}
@@ -128,7 +128,7 @@ class TeamsList extends PureComponent {
</View>
</View>
<FlatList
data={teamIds}
data={teams}
renderItem={this.renderItem}
keyExtractor={this.keyExtractor}
viewabilityConfig={{

View File

@@ -5,23 +5,19 @@ import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId, getTeam, makeGetBadgeCountForTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentTeamId, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import {removeProtocol} from 'app/utils/url';
import TeamsListItem from './teams_list_item.js';
function mapStateToProps(state, ownProps) {
const team = getTeam(state, ownProps.teamId);
const getMentionCount = makeGetBadgeCountForTeamId();
return {
currentTeamId: getCurrentTeamId(state),
currentUrl: removeProtocol(getCurrentUrl(state)),
displayName: team.display_name,
mentionCount: getMentionCount(state, ownProps.teamId),
name: team.name,
theme: getTheme(state)
teamMember: getTeamMemberships(state)[ownProps.team.id],
theme: getTheme(state),
...ownProps
};
}

View File

@@ -18,32 +18,29 @@ export default class TeamsListItem extends React.PureComponent {
static propTypes = {
currentTeamId: PropTypes.string.isRequired,
currentUrl: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
mentionCount: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
selectTeam: PropTypes.func.isRequired,
teamId: PropTypes.string.isRequired,
team: PropTypes.object.isRequired,
teamMember: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
selectTeam = wrapWithPreventDoubleTap(() => {
this.props.selectTeam(this.props.teamId);
this.props.selectTeam(this.props.team);
});
render() {
const {
currentTeamId,
currentUrl,
displayName,
mentionCount,
name,
teamId,
team,
teamMember,
theme
} = this.props;
const styles = getStyleSheet(theme);
let current;
if (teamId === currentTeamId) {
let badge;
if (team.id === currentTeamId) {
current = (
<View style={styles.checkmarkContainer}>
<IonIcon
@@ -54,15 +51,24 @@ export default class TeamsListItem extends React.PureComponent {
);
}
const badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={mentionCount}
minHeight={20}
minWidth={20}
/>
);
let badgeCount = 0;
if (teamMember.mention_count) {
badgeCount = teamMember.mention_count;
} else if (teamMember.msg_count) {
badgeCount = -1;
}
if (badgeCount) {
badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={badgeCount}
minHeight={20}
minWidth={20}
/>
);
}
return (
<View style={styles.teamWrapper}>
@@ -73,7 +79,7 @@ export default class TeamsListItem extends React.PureComponent {
<View style={styles.teamContainer}>
<View style={styles.teamIconContainer}>
<Text style={styles.teamIcon}>
{displayName.substr(0, 2).toUpperCase()}
{team.display_name.substr(0, 2).toUpperCase()}
</Text>
</View>
<View style={styles.teamNameContainer}>
@@ -82,14 +88,14 @@ export default class TeamsListItem extends React.PureComponent {
ellipsizeMode='tail'
style={styles.teamName}
>
{displayName}
{team.display_name}
</Text>
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={styles.teamUrl}
>
{`${currentUrl}/${name}`}
{`${currentUrl}/${team.name}`}
</Text>
</View>
{current}

View File

@@ -19,7 +19,7 @@ export default class ChannelIcon extends React.PureComponent {
static propTypes = {
isActive: PropTypes.bool,
isInfo: PropTypes.bool,
isUnread: PropTypes.bool,
hasUnread: PropTypes.bool,
membersCount: PropTypes.number,
size: PropTypes.number,
status: PropTypes.string,
@@ -30,12 +30,12 @@ export default class ChannelIcon extends React.PureComponent {
static defaultProps = {
isActive: false,
isInfo: false,
isUnread: false,
hasUnread: false,
size: 12
};
render() {
const {isActive, isUnread, isInfo, membersCount, size, status, theme, type} = this.props;
const {isActive, hasUnread, isInfo, membersCount, size, status, theme, type} = this.props;
const style = getStyleSheet(theme);
let activeIcon;
@@ -46,7 +46,7 @@ export default class ChannelIcon extends React.PureComponent {
let unreadGroup;
let offlineColor = changeOpacity(theme.sidebarText, 0.5);
if (isUnread) {
if (hasUnread) {
unreadIcon = style.iconUnread;
unreadGroupBox = style.groupBoxUnread;
unreadGroup = style.groupUnread;

View File

@@ -12,7 +12,6 @@ import {getFullName} from 'mattermost-redux/utils/user_utils';
import {General} from 'mattermost-redux/constants';
import {injectIntl, intlShape} from 'react-intl';
import Loading from 'app/components/loading';
import ProfilePicture from 'app/components/profile_picture';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -23,7 +22,6 @@ class ChannelIntro extends PureComponent {
currentChannel: PropTypes.object.isRequired,
currentChannelMembers: PropTypes.array.isRequired,
intl: intlShape.isRequired,
isLoadingPosts: PropTypes.bool,
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
@@ -306,35 +304,18 @@ class ChannelIntro extends PureComponent {
};
render() {
const {currentChannel, isLoadingPosts, theme} = this.props;
const {theme} = this.props;
const style = getStyleSheet(theme);
const channelType = currentChannel.type;
if (isLoadingPosts) {
return (
<View style={style.container}>
<Loading/>
</View>
);
}
let profiles;
if (channelType === General.DM_CHANNEL || channelType === General.GM_CHANNEL) {
profiles = (
<View>
<View style={style.profilesContainer}>
{this.buildProfiles()}
</View>
<View style={style.namesContainer}>
{this.buildNames()}
</View>
</View>
);
}
return (
<View style={style.container}>
{profiles}
<View style={style.profilesContainer}>
{this.buildProfiles()}
</View>
<View style={style.namesContainer}>
{this.buildNames()}
</View>
<View style={style.contentContainer}>
{this.buildContent()}
</View>

View File

@@ -1,19 +1,19 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {General, RequestStatus} from 'mattermost-redux/constants';
import {General} from 'mattermost-redux/constants';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUser, getProfilesInCurrentChannel} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelIntro from './channel_intro';
function mapStateToProps(state) {
const currentChannel = getCurrentChannel(state) || {};
const currentUser = getCurrentUser(state) || {};
const {status: getPostsRequestStatus} = state.requests.posts.getPosts;
let currentChannelMembers = [];
if (currentChannel.type === General.DM_CHANNEL) {
@@ -28,15 +28,20 @@ function mapStateToProps(state) {
}
const creator = currentChannel.creator_id === currentUser.id ? currentUser : state.entities.users.profiles[currentChannel.creator_id];
const postsInChannel = state.entities.posts.postsInChannel[currentChannel.id] || [];
return {
creator,
currentChannel,
currentChannelMembers,
isLoadingPosts: !postsInChannel.length && getPostsRequestStatus === RequestStatus.STARTED,
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(ChannelIntro);
function mapDispatchToProps(dispatch) {
// placeholder for invite and set header actions
return {
actions: bindActionCreators({}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChannelIntro);

View File

@@ -2,13 +2,14 @@
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelLoader from './channel_loader';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {deviceWidth} = state.device.dimension;
return {
...ownProps,
channelIsLoading: state.views.channel.loading,
deviceWidth,
theme: getTheme(state)

View File

@@ -1,231 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Alert,
Animated,
Linking,
TouchableOpacity,
View
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import FormattedText from 'app/components/formatted_text';
import {UpgradeTypes} from 'app/constants/view';
import checkUpgradeType from 'app/utils/client_upgrade';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const {View: AnimatedView} = Animated;
const UPDATE_TIMEOUT = 60000;
class ClientUpgradeListener extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
logError: PropTypes.func.isRequired,
setLastUpgradeCheck: PropTypes.func.isRequired
}).isRequired,
currentVersion: PropTypes.string,
downloadLink: PropTypes.string,
forceUpgrade: PropTypes.bool,
intl: intlShape.isRequired,
lastUpgradeCheck: PropTypes.number,
latestVersion: PropTypes.string,
minVersion: PropTypes.string,
navigator: PropTypes.object,
theme: PropTypes.object.isRequired
};
state = {
top: new Animated.Value(-100)
}
componentDidMount() {
const {forceUpgrade, lastUpgradeCheck, latestVersion, minVersion} = this.props;
if (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT) {
this.checkUpgrade(minVersion, latestVersion);
}
}
componentWillReceiveProps(nextProps) {
const {forceUpgrade, latestVersion, minVersion} = this.props;
const {latestVersion: nextLatestVersion, minVersion: nextMinVersion, lastUpgradeCheck} = nextProps;
const versionMismatch = latestVersion !== nextLatestVersion || minVersion !== nextMinVersion;
if (versionMismatch && (forceUpgrade || Date.now() - lastUpgradeCheck > UPDATE_TIMEOUT)) {
this.checkUpgrade(minVersion, latestVersion);
}
}
checkUpgrade = (minVersion, latestVersion) => {
const {actions, currentVersion} = this.props;
const upgradeType = checkUpgradeType(currentVersion, minVersion, latestVersion, actions.logError);
if (upgradeType === UpgradeTypes.NO_UPGRADE) {
return;
}
this.setState({upgradeType});
setTimeout(this.toggleUpgradeMessage, 500);
actions.setLastUpgradeCheck();
}
toggleUpgradeMessage = (show = true) => {
const toValue = show ? 75 : -100;
Animated.timing(this.state.top, {
toValue,
duration: 300
}).start();
}
handleDismiss = () => {
this.toggleUpgradeMessage(false);
}
handleDownload = () => {
const {downloadLink, intl} = this.props;
Linking.canOpenURL(downloadLink).then((supported) => {
if (supported) {
return Linking.openURL(downloadLink);
}
Alert.alert(
intl.formatMessage({
id: 'mobile.client_upgrade.download_error.title',
defaultMessage: 'Upgrade Error'
}),
intl.formatMessage({
id: 'mobile.client_upgrade.download_error.message',
defaultMessage: 'An error occurred while trying to open the download link.'
})
);
return false;
});
this.toggleUpgradeMessage(false);
}
handleLearnMore = () => {
this.props.navigator.dismissAllModals({animationType: 'none'});
this.props.navigator.showModal({
screen: 'ClientUpgrade',
navigatorStyle: {
navBarHidden: true,
statusBarHidden: true,
statusBarHideWithNavBar: true
},
passProps: {
upgradeType: this.state.upgradeType
}
});
this.toggleUpgradeMessage(false);
}
render() {
const {forceUpgrade, theme} = this.props;
const styles = getStyleSheet(theme);
return (
<AnimatedView
style={[styles.wrapper, {top: this.state.top}]}
>
<View style={styles.container}>
<View style={styles.message}>
<FormattedText
id='mobile.client_upgrade.listener.message'
defaultMessage='A client upgrade is available!'
style={styles.messageText}
/>
</View>
<View style={styles.bottom}>
<TouchableOpacity onPress={this.handleDownload}>
<FormattedText
style={styles.button}
id='mobile.client_upgrade.listener.upgrade_button'
defaultMessage='Upgrade'
/>
</TouchableOpacity>
<TouchableOpacity onPress={this.handleLearnMore}>
<FormattedText
style={styles.button}
id='mobile.client_upgrade.listener.learn_more_button'
defaultMessage='Learn More'
/>
</TouchableOpacity>
{!forceUpgrade &&
<TouchableOpacity onPress={this.handleDismiss}>
<FormattedText
style={styles.button}
id='mobile.client_upgrade.listener.dismiss_button'
defaultMessage='Dismiss'
/>
</TouchableOpacity>
}
</View>
</View>
</AnimatedView>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
bottom: {
flexDirection: 'row',
justifyContent: 'space-around',
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
backgroundColor: changeOpacity(theme.centerChannelColor, 0.06),
borderTopWidth: 1
},
button: {
color: theme.linkColor,
fontSize: 13,
paddingHorizontal: 5,
paddingVertical: 5
},
container: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelBg, 0.8),
borderRadius: 5
},
message: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
messageText: {
fontSize: 16,
color: changeOpacity(theme.centerChannelColor, 0.8),
fontWeight: '600'
},
wrapper: {
position: 'absolute',
elevation: 5,
left: 30,
right: 30,
height: 75,
backgroundColor: 'white',
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderWidth: 2,
borderRadius: 5,
shadowColor: theme.centerChannelColor,
shadowOffset: {
width: 0,
height: 3
},
shadowOpacity: 0.2,
shadowRadius: 2
}
};
});
export default injectIntl(ClientUpgradeListener);

View File

@@ -1,35 +0,0 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {logError} from 'mattermost-redux/actions/errors';
import {setLastUpgradeCheck} from 'app/actions/views/client_upgrade';
import getClientUpgrade from 'app/selectors/client_upgrade';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ClientUpgradeListener from './client_upgrade_listener';
function mapStateToProps(state) {
const {currentVersion, downloadLink, forceUpgrade, latestVersion, minVersion} = getClientUpgrade(state);
return {
currentVersion,
downloadLink,
forceUpgrade,
lastUpgradeCheck: state.views.clientUpgrade.lastUpdateCheck,
latestVersion,
minVersion,
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
logError,
setLastUpgradeCheck
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ClientUpgradeListener);

View File

@@ -3,7 +3,7 @@
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';

View File

@@ -3,7 +3,9 @@
import {connect} from 'react-redux';
import {getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import {getUser} from 'mattermost-redux/selectors/entities/users';
import UserListRow from './user_list_row';

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {Keyboard, Dimensions} from 'react-native';
import PropTypes from 'prop-types';
import BaseDrawer from 'react-native-drawer';
@@ -9,49 +8,20 @@ import BaseDrawer from 'react-native-drawer';
export default class Drawer extends BaseDrawer {
static propTypes = {
...BaseDrawer.propTypes,
onRequestClose: PropTypes.func.isRequired,
bottomPanOffset: PropTypes.number,
topPanOffset: PropTypes.number
onRequestClose: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.keyboardHeight = 0;
}
componentDidMount() {
Keyboard.addListener('keyboardDidShow', this.keyboardDidShow);
Keyboard.addListener('keyboardDidHide', this.keyboardDidHide);
}
componentWillUnmount() {
Keyboard.removeListener('keyboardDidShow', this.keyboardDidShow);
Keyboard.removeListener('keyboardDidHide', this.keyboardDidHide);
}
// To fix the android onLayout issue give this a value of 100% as it does not need another one
getMainHeight = () => '100%';
keyboardDidShow = (e) => {
this.keyboardHeight = e.endCoordinates.height;
};
keyboardDidHide = () => {
this.keyboardHeight = 0;
};
isOpened = () => {
return this._open; //eslint-disable-line no-underscore-dangle
};
processTapGestures = () => {
// Note that we explicitly don't support tap to open or double tap because I didn't copy them over
if (this._activeTween) { //eslint-disable-line no-underscore-dangle
if (this._activeTween) { // eslint-disable-line no-underscore-dangle
return false;
}
if (this.props.tapToClose && this._open) { //eslint-disable-line no-underscore-dangle
if (this.props.tapToClose && this._open) { // eslint-disable-line no-underscore-dangle
this.props.onRequestClose();
return true;
@@ -59,41 +29,4 @@ export default class Drawer extends BaseDrawer {
return false;
};
testPanResponderMask = (e) => {
if (this.props.disabled) {
return false;
}
// Disable if parent or child drawer exist and are open
if (this.context.drawer && this.context.drawer._open) { //eslint-disable-line no-underscore-dangle
return false;
}
if (this._childDrawer && this._childDrawer._open) { //eslint-disable-line no-underscore-dangle
return false;
}
const topPanOffset = this.props.topPanOffset || 0;
const bottomPanOffset = this.props.bottomPanOffset || 0;
const height = Dimensions.get('window').height;
if ((this.props.topPanOffset && e.nativeEvent.pageY < topPanOffset) ||
(this.props.bottomPanOffset && e.nativeEvent.pageY > (height - (bottomPanOffset + this.keyboardHeight)))) {
return false;
}
const pos0 = this.isLeftOrRightSide() ? e.nativeEvent.pageX : e.nativeEvent.pageY;
const deltaOpen = this.isLeftOrTopSide() ? this.getDeviceLength() - pos0 : pos0;
const deltaClose = this.isLeftOrTopSide() ? pos0 : this.getDeviceLength() - pos0;
if (this._open && deltaOpen > this.getOpenMask()) { //eslint-disable-line no-underscore-dangle
return false;
}
if (!this._open && deltaClose > this.getClosedMask()) { //eslint-disable-line no-underscore-dangle
return false;
}
return true;
};
}

View File

@@ -7,7 +7,7 @@ import {createSelector} from 'reselect';
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
import {getDimensions} from 'app/selectors/device';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import {CategoryNames, Emojis, EmojiIndicesByCategory} from 'app/utils/emojis';
import EmojiPicker from './emoji_picker';

View File

@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
import {Text} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import {GlobalStyles} from 'app/styles';
import {makeStyleSheetFromTheme} from 'app/utils/theme';

View File

@@ -7,7 +7,7 @@ import {connect} from 'react-redux';
import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files';
import {loadFilesForPostIfNecessary} from 'app/actions/views/channel';
import {addFileToFetchCache} from 'app/actions/views/file_preview';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import FileAttachmentList from './file_attachment_list';

View File

@@ -8,7 +8,7 @@ import {createSelector} from 'reselect';
import {handleRemoveFile, retryFileUpload} from 'app/actions/views/file_upload';
import {addFileToFetchCache} from 'app/actions/views/file_preview';
import {getDimensions} from 'app/selectors/device';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import FileUploadPreview from './file_upload_preview';

View File

@@ -24,7 +24,7 @@ class FormattedTime extends React.PureComponent {
Reflect.deleteProperty(props, 'format');
const formattedTime = intl.formatDate(value, {...props, hour: 'numeric', minute: 'numeric'});
const formattedTime = intl.formatTime(value, this.props);
if (typeof children === 'function') {
return children(formattedTime);
@@ -35,3 +35,4 @@ class FormattedTime extends React.PureComponent {
}
export default injectIntl(FormattedTime);

View File

@@ -3,9 +3,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {FlatList, Platform, ScrollView, StyleSheet, View} from 'react-native';
import RefreshList from 'app/components/refresh_list';
import {FlatList, Platform, RefreshControl, ScrollView, StyleSheet, View} from 'react-native';
import VirtualList from './virtual_list';
@@ -66,7 +64,7 @@ export default class InvertibleFlatList extends PureComponent {
<ScrollView
{...props}
refreshControl={
<RefreshList
<RefreshControl
refreshing={props.refreshing}
onRefresh={props.onRefresh}
tintColor={theme.centerChannelColor}
@@ -133,10 +131,7 @@ const styles = StyleSheet.create({
},
vertical: Platform.select({
android: {
transform: [
{perspective: 1},
{scaleY: -1}
]
scaleY: -1
},
ios: {
transform: [{scaleY: -1}]
@@ -144,10 +139,7 @@ const styles = StyleSheet.create({
}),
horizontal: Platform.select({
android: {
transform: [
{perspective: 1},
{scaleY: -1}
]
scaleX: -1
},
ios: {
transform: [{scaleX: -1}]

View File

@@ -1,24 +1,19 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {KeyboardAvoidingView, Platform, View} from 'react-native';
export default class KeyboardLayout extends PureComponent {
export default class KeyboardLayout extends React.PureComponent {
static propTypes = {
behaviour: PropTypes.string,
children: PropTypes.node,
keyboardVerticalOffset: PropTypes.number,
statusBarHeight: PropTypes.number
};
static defaultProps = {
keyboardVerticalOffset: 0
keyboardVerticalOffset: PropTypes.number
};
render() {
const {behaviour, children, keyboardVerticalOffset, statusBarHeight, ...otherProps} = this.props;
const {behaviour, children, keyboardVerticalOffset, ...otherProps} = this.props;
if (Platform.OS === 'android') {
return (
@@ -28,17 +23,10 @@ export default class KeyboardLayout extends PureComponent {
);
}
let height = 0;
if (statusBarHeight > 20) {
height = (statusBarHeight - 20) + keyboardVerticalOffset;
} else {
height = keyboardVerticalOffset;
}
return (
<KeyboardAvoidingView
behaviour={behaviour}
keyboardVerticalOffset={height}
keyboardVerticalOffset={keyboardVerticalOffset}
{...otherProps}
>
{children}

View File

@@ -1,16 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getStatusBarHeight} from 'app/selectors/device';
import KeyboardLayout from './keyboard_layout';
function mapStateToProps(state) {
return {
statusBarHeight: getStatusBarHeight(state)
};
}
export default connect(mapStateToProps)(KeyboardLayout);

View File

@@ -3,7 +3,7 @@
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import MarkdownCodeBlock from './markdown_code_block';

View File

@@ -1,13 +1,11 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Children, PureComponent} from 'react';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Linking, Text} from 'react-native';
import urlParse from 'url-parse';
import CustomPropTypes from 'app/constants/custom_prop_types';
import Config from 'assets/config';
export default class MarkdownLink extends PureComponent {
static propTypes = {
@@ -22,7 +20,13 @@ export default class MarkdownLink extends PureComponent {
handlePress = () => {
// Android doesn't like the protocol being upper case
const url = this.props.href;
let url = this.props.href;
const index = url.indexOf(':');
if (index !== -1) {
const protocol = url.substring(0, index);
url = protocol.toLowerCase() + url.substring(index);
}
Linking.canOpenURL(url).then((supported) => {
if (supported) {
@@ -31,49 +35,13 @@ export default class MarkdownLink extends PureComponent {
});
};
parseLinkLiteral = (literal) => {
let nextLiteral = literal;
const WWW_REGEX = /\b^(?:www.)/i;
if (nextLiteral.match(WWW_REGEX)) {
nextLiteral = literal.replace(WWW_REGEX, 'www.');
}
const parsed = urlParse(nextLiteral, {});
return parsed.href;
}
parseChildren = () => {
return Children.map(this.props.children, (child) => {
if (!child.props.literal || typeof child.props.literal !== 'string' || (child.props.context && child.props.context.length && !child.props.context.includes('link'))) {
return child;
}
const {props, ...otherChildProps} = child;
const {literal, ...otherProps} = props;
const nextProps = {
literal: this.parseLinkLiteral(literal),
...otherProps
};
return {
props: nextProps,
...otherChildProps
};
});
}
render() {
const children = Config.ExperimentalNormalizeMarkdownLinks ? this.parseChildren() : this.props.children;
return (
<Text
onPress={this.handlePress}
onLongPress={this.props.onLongPress}
>
{children}
{this.props.children}
</Text>
);
}

View File

@@ -1,22 +1,37 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {close as closeWebSocket, init as initWebSocket} from 'mattermost-redux/actions/websocket';
import {getConnection} from 'app/selectors/device';
import OfflineIndicator from './offline_indicator';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {websocket} = state.requests.general;
const {appState} = state.entities.general;
const webSocketStatus = websocket.status;
const isConnecting = websocket.error > 1;
const isConnecting = websocket.error >= 2;
return {
appState,
isConnecting,
isOnline: getConnection(state),
webSocketStatus
webSocketStatus,
...ownProps
};
}
export default connect(mapStateToProps)(OfflineIndicator);
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
closeWebSocket,
initWebSocket
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(OfflineIndicator);

View File

@@ -1,13 +1,14 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component} from 'react';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
ActivityIndicator,
Animated,
Platform,
StyleSheet,
TouchableOpacity,
View
} from 'react-native';
import IonIcon from 'react-native-vector-icons/Ionicons';
@@ -23,14 +24,20 @@ const OFFLINE = 'offline';
const CONNECTING = 'connecting';
const CONNECTED = 'connected';
export default class OfflineIndicator extends Component {
export default class OfflineIndicator extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
closeWebSocket: PropTypes.func.isRequired,
initWebSocket: PropTypes.func.isRequired
}).isRequired,
appState: PropTypes.bool,
isConnecting: PropTypes.bool,
isOnline: PropTypes.bool,
webSocketStatus: PropTypes.string
};
static defaultProps: {
appState: true,
isOnline: true
};
@@ -46,30 +53,42 @@ export default class OfflineIndicator extends Component {
}
componentWillReceiveProps(nextProps) {
const {webSocketStatus} = this.props;
if (nextProps.isOnline) {
if (this.state.network && webSocketStatus === RequestStatus.STARTED && nextProps.webSocketStatus === RequestStatus.SUCCESS) {
// Show the connected animation only if we had a previous network status
this.connected();
} else if (webSocketStatus === RequestStatus.STARTED && nextProps.webSocketStatus === RequestStatus.FAILURE && nextProps.isConnecting) {
// Show the connecting bar if it failed to connect at least twice
this.connecting();
const {appState, isConnecting, isOnline, webSocketStatus} = nextProps;
if (appState) { // The app is in the foreground
if (isOnline) {
if (this.state.network && webSocketStatus === RequestStatus.SUCCESS) {
// Show the connected animation only if we had a previous network status
this.connected();
} else if ((webSocketStatus === RequestStatus.STARTED || webSocketStatus === RequestStatus.FAILURE) && isConnecting) {
// Show the connecting bar if it failed to connect at least twice
this.connecting();
}
} else {
this.offline();
}
} else {
this.offline();
}
}
shouldComponentUpdate(nextProps, nextState) {
return nextState.network !== this.state.network && nextState.network;
}
offline = () => {
this.setState({network: OFFLINE}, () => {
this.show();
});
};
connect = () => {
const {actions, isOnline, webSocketStatus} = this.props;
const {closeWebSocket, initWebSocket} = actions;
initWebSocket(Platform.OS);
// close the WS connection after trying for 5 seconds
setTimeout(() => {
if (webSocketStatus !== RequestStatus.SUCCESS) {
closeWebSocket(true);
this.setState({network: isOnline ? OFFLINE : CONNECTING});
}
}, 5000);
};
connecting = () => {
const prevState = this.state.network;
this.setState({network: CONNECTING}, () => {
@@ -92,7 +111,7 @@ export default class OfflineIndicator extends Component {
this.state.top, {
toValue: INITIAL_TOP,
duration: 300,
delay: 500
delay: 1000
}
)
]).start(() => {
@@ -127,6 +146,18 @@ export default class OfflineIndicator extends Component {
case OFFLINE:
i18nId = 'mobile.offlineIndicator.offline';
defaultMessage = 'No internet connection';
action = (
<TouchableOpacity
onPress={this.connect}
style={[styles.actionContainer, styles.actionButton]}
>
<IonIcon
color='#FFFFFF'
name='ios-refresh'
size={20}
/>
</TouchableOpacity>
);
break;
case CONNECTING:
i18nId = 'mobile.offlineIndicator.connecting';

View File

@@ -8,52 +8,30 @@ import {addReaction, createPost, deletePost, removePost} from 'mattermost-redux/
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {insertToDraft, setPostTooltipVisible} from 'app/actions/views/channel';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {setPostTooltipVisible} from 'app/actions/views/channel';
import {getTheme} from 'app/selectors/preferences';
import Post from './post';
function mapStateToProps(state, ownProps) {
const post = getPost(state, ownProps.postId);
function makeMapStateToProps() {
return function mapStateToProps(state, ownProps) {
const post = getPost(state, ownProps.post.id);
const {config, license} = state.entities.general;
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
const {config, license} = state.entities.general;
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
const {tooltipVisible} = state.views.channel;
let isFirstReply = true;
let isLastReply = true;
let commentedOnPost = null;
if (ownProps.renderReplies && post && post.root_id) {
if (ownProps.previousPostId) {
const previousPost = getPost(state, ownProps.previousPostId);
if (previousPost && (previousPost.id === post.root_id || previousPost.root_id === post.root_id)) {
// Previous post is root post or previous post is in same thread
isFirstReply = false;
} else {
// Last post is not a comment on the same message
commentedOnPost = getPost(state, post.root_id);
}
}
if (ownProps.nextPostId) {
const nextPost = getPost(state, ownProps.nextPostId);
if (nextPost && nextPost.root_id === post.root_id) {
isLastReply = false;
}
}
}
return {
config,
currentUserId: getCurrentUserId(state),
post,
isFirstReply,
isLastReply,
commentedOnPost,
license,
roles,
theme: getTheme(state)
return {
...ownProps,
post,
config,
currentUserId: getCurrentUserId(state),
highlight: ownProps.post.highlight,
license,
roles,
theme: getTheme(state),
tooltipVisible
};
};
}
@@ -64,10 +42,9 @@ function mapDispatchToProps(dispatch) {
createPost,
deletePost,
removePost,
setPostTooltipVisible,
insertToDraft
setPostTooltipVisible
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Post);
export default connect(makeMapStateToProps, mapDispatchToProps)(Post);

View File

@@ -10,7 +10,6 @@ import {
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import {isToolTipShowing} from 'react-native-tooltip';
import PostBody from 'app/components/post_body';
import PostHeader from 'app/components/post_header';
@@ -26,24 +25,21 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {canDeletePost, canEditPost, isPostEphemeral, isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
import Config from 'assets/config';
class Post extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
addReaction: PropTypes.func.isRequired,
createPost: PropTypes.func.isRequired,
deletePost: PropTypes.func.isRequired,
insertToDraft: PropTypes.func.isRequired,
removePost: PropTypes.func.isRequired
removePost: PropTypes.func.isRequired,
setPostTooltipVisible: PropTypes.func.isRequired
}).isRequired,
config: PropTypes.object.isRequired,
currentUserId: PropTypes.string.isRequired,
highlight: PropTypes.bool,
intl: intlShape.isRequired,
style: ViewPropTypes.style,
post: PropTypes.object,
postId: PropTypes.string.isRequired, // Used by container // eslint-disable-line no-unused-prop-types
post: PropTypes.object.isRequired,
renderReplies: PropTypes.bool,
isFirstReply: PropTypes.bool,
isLastReply: PropTypes.bool,
@@ -54,6 +50,7 @@ class Post extends PureComponent {
roles: PropTypes.string,
shouldRenderReplyButton: PropTypes.bool,
showFullDate: PropTypes.bool,
tooltipVisible: PropTypes.bool,
theme: PropTypes.object.isRequired,
onPress: PropTypes.func,
onReply: PropTypes.func
@@ -68,32 +65,18 @@ class Post extends PureComponent {
const {config, license, currentUserId, roles, post} = props;
this.editDisableAction = new DelayedAction(this.handleEditDisable);
if (post) {
this.state = {
canEdit: canEditPost(config, license, currentUserId, post, this.editDisableAction),
canDelete: canDeletePost(config, license, currentUserId, post, isAdmin(roles), isSystemAdmin(roles))
};
} else {
this.state = {
canEdit: false,
canDelete: false
};
}
this.state = {
canEdit: canEditPost(config, license, currentUserId, post, this.editDisableAction),
canDelete: canDeletePost(config, license, currentUserId, post, isAdmin(roles), isSystemAdmin(roles))
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.config !== this.props.config ||
nextProps.license !== this.props.license ||
nextProps.currentUserId !== this.props.currentUserId ||
nextProps.post !== this.props.post ||
nextProps.roles !== this.props.roles) {
const {config, license, currentUserId, roles, post} = nextProps;
this.setState({
canEdit: canEditPost(config, license, currentUserId, post, this.editDisableAction),
canDelete: canDeletePost(config, license, currentUserId, post, isAdmin(roles), isSystemAdmin(roles))
});
}
const {config, license, currentUserId, roles, post} = nextProps;
this.setState({
canEdit: canEditPost(config, license, currentUserId, post, this.editDisableAction),
canDelete: canDeletePost(config, license, currentUserId, post, isAdmin(roles), isSystemAdmin(roles))
});
}
componentWillUnmount() {
@@ -119,12 +102,6 @@ class Post extends PureComponent {
});
};
autofillUserMention = (username) => {
// create a general action that checks for currentThreadId in the state and decides
// whether to insert to root or thread
this.props.actions.insertToDraft(`@${username} `);
}
handleEditDisable = () => {
this.setState({canEdit: false});
};
@@ -251,8 +228,8 @@ class Post extends PureComponent {
};
handlePress = () => {
const {post, onPress} = this.props;
if (!isToolTipShowing) {
const {post, onPress, tooltipVisible} = this.props;
if (!tooltipVisible) {
if (onPress && post.state !== Posts.POST_DELETED && !isSystemMessage(post) && !isPostPendingOrFailed(post)) {
preventDoubleTap(onPress, null, post);
} else if (isPostEphemeral(post)) {
@@ -262,8 +239,8 @@ class Post extends PureComponent {
};
handleReply = () => {
const {post, onReply} = this.props;
if (!isToolTipShowing && onReply) {
const {post, onReply, tooltipVisible} = this.props;
if (!tooltipVisible && onReply) {
return preventDoubleTap(onReply, null, post);
}
@@ -304,17 +281,18 @@ class Post extends PureComponent {
};
viewUserProfile = () => {
const {isSearchResult} = this.props;
const {isSearchResult, tooltipVisible} = this.props;
if (!isSearchResult && !isToolTipShowing) {
if (!isSearchResult && !tooltipVisible) {
preventDoubleTap(this.goToUserProfile, this);
}
};
toggleSelected = (selected) => {
if (!isToolTipShowing) {
this.setState({selected});
toggleSelected = (selected, tooltip) => {
if (tooltip) {
this.props.actions.setPostTooltipVisible(selected);
}
this.setState({selected});
};
render() {
@@ -329,17 +307,10 @@ class Post extends PureComponent {
showFullDate,
theme
} = this.props;
if (!post) {
return null;
}
const style = getStyleSheet(theme);
const selected = this.state && this.state.selected ? style.selected : null;
const highlighted = highlight ? style.highlight : null;
const onUsernamePress = Config.ExperimentalUsernamePressIsMention ? this.autofillUserMention : this.viewUserProfile;
return (
<View style={[style.container, this.props.style, highlighted, selected]}>
<View style={[style.profilePictureContainer, (isPostPendingOrFailed(post) && style.pendingPost)]}>
@@ -359,7 +330,7 @@ class Post extends PureComponent {
shouldRenderReplyButton={shouldRenderReplyButton}
showFullDate={showFullDate}
onPress={this.handleReply}
onUsernamePress={onUsernamePress}
onViewUserProfile={this.viewUserProfile}
renderReplies={renderReplies}
theme={theme}
/>

View File

@@ -6,8 +6,17 @@ import {bindActionCreators} from 'redux';
import {getOpenGraphMetadata} from 'mattermost-redux/actions/posts';
import {getTheme} from 'app/selectors/preferences';
import PostAttachmentOpenGraph from './post_attachment_opengraph';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
@@ -16,4 +25,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(null, mapDispatchToProps)(PostAttachmentOpenGraph);
export default connect(mapStateToProps, mapDispatchToProps)(PostAttachmentOpenGraph);

View File

@@ -40,11 +40,6 @@ export default class PostAttachmentOpenGraph extends PureComponent {
};
}
componentDidMount() {
this.mounted = true;
this.getBestImageUrl(this.props.openGraphData);
}
componentWillMount() {
this.fetchData(this.props.link, this.props.openGraphData);
}
@@ -54,14 +49,6 @@ export default class PostAttachmentOpenGraph extends PureComponent {
this.setState({imageLoaded: false});
this.fetchData(nextProps.link, nextProps.openGraphData);
}
if (this.props.openGraphData !== nextProps.openGraphData) {
this.getBestImageUrl(nextProps.openGraphData);
}
}
componentWillUnmount() {
this.mounted = false;
}
calculateLargeImageDimensions = (width, height) => {
@@ -113,8 +100,8 @@ export default class PostAttachmentOpenGraph extends PureComponent {
}
getBestImageUrl(data) {
if (!data || !data.images) {
return;
if (!data.images) {
return null;
}
const bestDimensions = {
@@ -122,11 +109,7 @@ export default class PostAttachmentOpenGraph extends PureComponent {
height: MAX_IMAGE_HEIGHT
};
const bestImage = getNearestPoint(bestDimensions, data.images, 'width', 'height');
const imageUrl = bestImage.secure_url || bestImage.url;
if (imageUrl) {
this.getImageSize(imageUrl);
}
return bestImage.secure_url || bestImage.url;
}
getImageSize = (imageUrl) => {
@@ -143,14 +126,11 @@ export default class PostAttachmentOpenGraph extends PureComponent {
} else {
dimensions = this.calculateSmallImageDimensions(width, height);
}
if (this.mounted) {
this.setState({
...dimensions,
hasLargeImage: isLarge,
imageLoaded: true,
imageUrl
});
}
this.setState({
...dimensions,
hasLargeImage: isLarge,
imageLoaded: true
});
}, () => null);
}
};
@@ -161,14 +141,18 @@ export default class PostAttachmentOpenGraph extends PureComponent {
render() {
const {openGraphData, theme} = this.props;
const {hasLargeImage, height, imageLoaded, imageUrl, offset, width} = this.state;
const {hasLargeImage, height, imageLoaded, offset, width} = this.state;
if (!openGraphData || !openGraphData.description) {
return null;
}
const style = getStyleSheet(theme);
const imageUrl = this.getBestImageUrl(openGraphData);
const isThumbnail = !hasLargeImage && imageLoaded;
if (imageUrl) {
this.getImageSize(imageUrl);
}
return (
<View style={style.container}>

View File

@@ -7,9 +7,11 @@ import {bindActionCreators} from 'redux';
import {flagPost, unflagPost} from 'mattermost-redux/actions/posts';
import {Posts} from 'mattermost-redux/constants';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {getMyPreferences, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
import {isPostFlagged, isPostEphemeral, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import {getTheme} from 'app/selectors/preferences';
import PostBody from './post_body';
function mapStateToProps(state, ownProps) {

View File

@@ -190,20 +190,11 @@ class PostBody extends PureComponent {
let messageComponent;
if (hasBeenDeleted) {
messageComponent = (
<TouchableHighlight
onHideUnderlay={this.handleHideUnderlay}
onPress={onPress}
onShowUnderlay={this.handleShowUnderlay}
underlayColor='transparent'
>
<View style={{flexDirection: 'row'}}>
<FormattedText
style={messageStyle}
id='post_body.deleted'
defaultMessage='(message deleted)'
/>
</View>
</TouchableHighlight>
<FormattedText
style={messageStyle}
id='post_body.deleted'
defaultMessage='(message deleted)'
/>
);
body = (<View>{messageComponent}</View>);
} else if (message.length) {

View File

@@ -9,10 +9,11 @@ import {connect} from 'react-redux';
import {Preferences} from 'mattermost-redux/constants';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getOpenGraphMetadataForUrl} from 'mattermost-redux/selectors/entities/posts';
import {getBool, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import {ViewTypes} from 'app/constants';
import {getDimensions} from 'app/selectors/device';
import {getTheme} from 'app/selectors/preferences';
import {extractFirstLink} from 'app/utils/url';
import PostBodyAdditionalContent from './post_body_additional_content';
@@ -40,6 +41,7 @@ function makeMapStateToProps() {
const link = getFirstLink(ownProps.message);
return {
...ownProps,
...getDimensions(state),
config,
link,

View File

@@ -5,7 +5,6 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Image,
ImageBackground,
Linking,
Platform,
StyleSheet,
@@ -99,7 +98,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
return null;
}
const {link, openGraphData, showLinkPreviews, theme} = this.props;
const {link, openGraphData, showLinkPreviews} = this.props;
const attachments = this.getSlackAttachment();
if (attachments) {
return attachments;
@@ -110,7 +109,6 @@ export default class PostBodyAdditionalContent extends PureComponent {
<PostAttachmentOpenGraph
link={link}
openGraphData={openGraphData}
theme={theme}
/>
);
}
@@ -186,7 +184,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
{...this.responder}
onPress={this.playYouTubeVideo}
>
<ImageBackground
<Image
style={[styles.image, {width, height}]}
source={{uri: imgUrl}}
resizeMode={'cover'}
@@ -198,7 +196,7 @@ export default class PostBodyAdditionalContent extends PureComponent {
onPress={this.playYouTubeVideo}
/>
</TouchableWithoutFeedback>
</ImageBackground>
</Image>
</TouchableWithoutFeedback>
);
}
@@ -246,22 +244,22 @@ export default class PostBodyAdditionalContent extends PureComponent {
};
render() {
const {link, openGraphData, postProps} = this.props;
const {link, openGraphData} = this.props;
const {linkLoaded, linkLoadError} = this.state;
const {attachments} = postProps;
let isYouTube = false;
let isImage = false;
let isOpenGraph = false;
if (!link && !attachments) {
return null;
}
if (link) {
isYouTube = isYoutubeLink(link);
isImage = isImageLink(link);
isOpenGraph = Boolean(openGraphData && openGraphData.description);
const isYouTube = isYoutubeLink(link);
const isImage = isImageLink(link);
const isOpenGraph = Boolean(openGraphData && openGraphData.description);
if (((isImage && !isOpenGraph) || isYouTube) && !linkLoadError) {
const embed = this.generateToggleableEmbed(isImage, isYouTube);
if (embed && (linkLoaded || isYouTube)) {
return embed;
if (((isImage && !isOpenGraph) || isYouTube) && !linkLoadError) {
const embed = this.generateToggleableEmbed(isImage, isYouTube);
if (embed && (linkLoaded || isYouTube)) {
return embed;
}
}
}

View File

@@ -5,11 +5,13 @@ import {connect} from 'react-redux';
import {Preferences} from 'mattermost-redux/constants';
import {getPost, makeGetCommentCountForPost} from 'mattermost-redux/selectors/entities/posts';
import {getBool, getTeammateNameDisplaySetting, getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getBool, getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
import {getUser} from 'mattermost-redux/selectors/entities/users';
import {isPostPendingOrFailed, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import {getTheme} from 'app/selectors/preferences';
import PostHeader from './post_header';
function makeMapStateToProps() {
@@ -18,7 +20,7 @@ function makeMapStateToProps() {
const {config} = state.entities.general;
const post = getPost(state, ownProps.postId);
const commentedOnUser = getUser(state, ownProps.commentedOnUserId);
const user = getUser(state, post.user_id) || {};
const user = getUser(state, post.user_id);
const teammateNameDisplay = getTeammateNameDisplaySetting(state);
const militaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time');
@@ -34,8 +36,7 @@ function makeMapStateToProps() {
isPendingOrFailedPost: isPostPendingOrFailed(post),
isSystemMessage: isSystemMessage(post),
overrideUsername: post.props && post.props.override_username,
theme: getTheme(state),
username: user.username
theme: getTheme(state)
};
};
}

View File

@@ -31,30 +31,26 @@ export default class PostHeader extends PureComponent {
isSystemMessage: PropTypes.bool,
militaryTime: PropTypes.bool,
onPress: PropTypes.func,
onUsernamePress: PropTypes.func,
onViewUserProfile: PropTypes.func,
overrideUsername: PropTypes.string,
renderReplies: PropTypes.bool,
shouldRenderReplyButton: PropTypes.bool,
showFullDate: PropTypes.bool,
theme: PropTypes.object.isRequired,
username: PropTypes.string.isRequired
theme: PropTypes.object.isRequired
};
static defaultProps = {
commentCount: 0,
onPress: emptyFunction,
onUsernamePress: emptyFunction
onViewUserProfile: emptyFunction
};
handleUsernamePress = () => {
this.props.onUsernamePress(this.props.username);
}
getDisplayName = (style) => {
const {
enablePostUsernameOverride,
fromWebHook,
isSystemMessage,
onViewUserProfile,
overrideUsername
} = this.props;
@@ -84,7 +80,7 @@ export default class PostHeader extends PureComponent {
);
} else if (this.props.displayName) {
return (
<TouchableOpacity onPress={this.handleUsernamePress}>
<TouchableOpacity onPress={onViewUserProfile}>
<Text style={style.displayName}>
{this.props.displayName}
</Text>

View File

@@ -5,20 +5,14 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {refreshChannelWithRetry} from 'app/actions/views/channel';
import {makePreparePostIdsForPostList} from 'app/selectors/post_list';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import PostList from './post_list';
function makeMapStateToProps() {
const preparePostIds = makePreparePostIdsForPostList();
return (state, ownProps) => {
return {
postIds: preparePostIds(state, ownProps),
theme: getTheme(state)
};
function mapStateToProps(state, ownProps) {
return {
...ownProps,
theme: getTheme(state)
};
}
@@ -30,4 +24,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(makeMapStateToProps, mapDispatchToProps)(PostList);
export default connect(mapStateToProps, mapDispatchToProps)(PostList);

View File

@@ -2,7 +2,6 @@
// See License.txt for license information.
import React, {PureComponent} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {
TouchableOpacity,
@@ -14,7 +13,7 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
import FormattedText from 'app/components/formatted_text';
class LoadMorePosts extends PureComponent {
export default class LoadMorePosts extends PureComponent {
static propTypes = {
loading: PropTypes.bool.isRequired,
loadMore: PropTypes.func,
@@ -73,11 +72,3 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
}
};
});
function mapStateToProps(state, ownProps) {
return {
loading: state.views.channel.loadingPosts[ownProps.channelId] || false
};
}
export default connect(mapStateToProps)(LoadMorePosts);

View File

@@ -7,16 +7,19 @@ import {
StyleSheet,
View
} from 'react-native';
import FlatList from 'app/components/inverted_flat_list';
import {General} from 'mattermost-redux/constants';
import {addDatesToPostList} from 'mattermost-redux/utils/post_utils';
import ChannelIntro from 'app/components/channel_intro';
import FlatList from 'app/components/inverted_flat_list';
import Post from 'app/components/post';
import {DATE_LINE, START_OF_NEW_MESSAGES} from 'app/selectors/post_list';
import DateHeader from './date_header';
import LoadMorePosts from './load_more_posts';
import NewMessagesDivider from './new_messages_divider';
const LOAD_MORE_POSTS = 'load-more-posts';
export default class PostList extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
@@ -24,39 +27,49 @@ export default class PostList extends PureComponent {
}).isRequired,
channelId: PropTypes.string,
currentUserId: PropTypes.string,
highlightPostId: PropTypes.string,
indicateNewMessages: PropTypes.bool,
isLoadingMore: PropTypes.bool,
isSearchResult: PropTypes.bool,
lastViewedAt: PropTypes.number, // Used by container // eslint-disable-line no-unused-prop-types
lastViewedAt: PropTypes.number,
loadMore: PropTypes.func,
navigator: PropTypes.object,
onPostPress: PropTypes.func,
onRefresh: PropTypes.func,
postIds: PropTypes.array.isRequired,
posts: PropTypes.array.isRequired,
refreshing: PropTypes.bool,
renderReplies: PropTypes.bool,
showLoadMore: PropTypes.bool,
shouldRenderReplyButton: PropTypes.bool,
theme: PropTypes.object.isRequired
};
static defaultProps = {
loadMore: () => true
getPostsWithDates = () => {
const {posts, indicateNewMessages, currentUserId, lastViewedAt, showLoadMore} = this.props;
const list = addDatesToPostList(posts, {indicateNewMessages, currentUserId, lastViewedAt});
if (showLoadMore) {
return [...list, LOAD_MORE_POSTS];
}
return list;
};
componentDidUpdate(prevProps) {
if (prevProps.channelId !== this.props.channelId && this.refs.list) {
// When switching channels make sure we start from the bottom
this.refs.list.scrollToOffset({y: 0, animated: false});
}
}
getItem = (data, index) => data[index];
getItemCount = (data) => data.length;
keyExtractor = (item) => {
// All keys are strings (either post IDs or special keys)
return item;
if (item instanceof Date) {
return item.getTime();
}
if (item === General.START_OF_NEW_MESSAGES || item === LOAD_MORE_POSTS) {
return item;
}
return item.id;
};
loadMorePosts = () => {
const {loadMore, isLoadingMore} = this.props;
if (typeof loadMore === 'function' && !isLoadingMore) {
loadMore();
}
};
onRefresh = () => {
@@ -75,26 +88,18 @@ export default class PostList extends PureComponent {
}
};
renderItem = ({item, index}) => {
if (item === START_OF_NEW_MESSAGES) {
renderChannelIntro = () => {
const {channelId, navigator, refreshing, showLoadMore} = this.props;
if (channelId && !showLoadMore && !refreshing) {
return (
<NewMessagesDivider
theme={this.props.theme}
/>
<View>
<ChannelIntro navigator={navigator}/>
</View>
);
} else if (item.indexOf(DATE_LINE) === 0) {
const date = item.substring(DATE_LINE.length);
return this.renderDateHeader(new Date(date));
}
const postId = item;
// Remember that the list is rendered with item 0 at the bottom so the "previous" post
// comes after this one in the list
const previousPostId = index < this.props.postIds.length - 1 ? this.props.postIds[index + 1] : null;
const nextPostId = index > 0 ? this.props.postIds[index - 1] : null;
return this.renderPost(postId, previousPostId, nextPostId);
return null;
};
renderDateHeader = (date) => {
@@ -106,9 +111,35 @@ export default class PostList extends PureComponent {
);
};
renderPost = (postId, previousPostId, nextPostId) => {
renderItem = ({item}) => {
if (item instanceof Date) {
return this.renderDateHeader(item);
}
if (item === General.START_OF_NEW_MESSAGES) {
return (
<NewMessagesDivider
theme={this.props.theme}
/>
);
}
if (item === LOAD_MORE_POSTS && this.props.showLoadMore) {
return (
<LoadMorePosts
loading={this.props.isLoadingMore}
theme={this.props.theme}
/>
);
}
return this.renderPost(item);
};
getItem = (data, index) => data[index];
getItemCount = (data) => data.length;
renderPost = (post) => {
const {
highlightPostId,
isSearchResult,
navigator,
onPostPress,
@@ -118,45 +149,24 @@ export default class PostList extends PureComponent {
return (
<Post
postId={postId}
previousPostId={previousPostId}
nextPostId={nextPostId}
highlight={highlightPostId && highlightPostId === postId}
post={post}
renderReplies={renderReplies}
isFirstReply={post.isFirstReply}
isLastReply={post.isLastReply}
isSearchResult={isSearchResult}
shouldRenderReplyButton={shouldRenderReplyButton}
commentedOnPost={post.commentedOnPost}
onPress={onPostPress}
navigator={navigator}
/>
);
};
renderFooter = () => {
if (this.props.showLoadMore) {
return <LoadMorePosts theme={this.props.theme}/>;
} else if (this.props.channelId) {
// FIXME: Only show the channel intro when we are at the very start of the channel
return (
<View>
<ChannelIntro navigator={this.props.navigator}/>
</View>
);
}
return null;
};
render() {
const {
channelId,
highlightPostId,
loadMore,
postIds,
theme
} = this.props;
const {channelId, refreshing, theme} = this.props;
const refreshControl = {
refreshing: false
refreshing
};
if (channelId) {
@@ -165,14 +175,12 @@ export default class PostList extends PureComponent {
return (
<FlatList
ref='list'
data={postIds}
extraData={highlightPostId}
initialNumToRender={15}
data={this.getPostsWithDates()}
initialNumToRender={20}
inverted={true}
keyExtractor={this.keyExtractor}
ListFooterComponent={this.renderFooter}
onEndReached={loadMore}
ListFooterComponent={this.renderChannelIntro}
onEndReached={this.loadMorePosts}
onEndReachedThreshold={0}
{...refreshControl}
renderItem={this.renderItem}

View File

@@ -6,7 +6,7 @@ import {connect} from 'react-redux';
import {getPost} from 'mattermost-redux/selectors/entities/posts';
import {isSystemMessage} from 'mattermost-redux/utils/post_utils';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import PostProfilePicture from './post_profile_picture';

View File

@@ -5,7 +5,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import {Image, TouchableOpacity, View} from 'react-native';
import AppIcon from 'app/components/app_icon';
import MattermostIcon from 'app/components/mattermost_icon';
import ProfilePicture from 'app/components/profile_picture';
import {emptyFunction} from 'app/utils/general';
import webhookIcon from 'assets/images/icons/webhook.jpg';
@@ -25,7 +25,7 @@ function PostProfilePicture(props) {
if (isSystemMessage) {
return (
<View>
<AppIcon
<MattermostIcon
color={theme.centerChannelColor}
height={PROFILE_PICTURE_SIZE}
width={PROFILE_PICTURE_SIZE}

View File

@@ -1,6 +1,5 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {injectIntl, intlShape} from 'react-intl';
import {
Platform,
StyleSheet,
@@ -11,38 +10,21 @@ import ImagePicker from 'react-native-image-picker';
import {changeOpacity} from 'app/utils/theme';
class AttachmentButton extends PureComponent {
export default class AttachmentButton extends PureComponent {
static propTypes = {
blurTextBox: PropTypes.func.isRequired,
intl: intlShape.isRequired,
navigator: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
uploadFiles: PropTypes.func.isRequired
};
attachFileFromCamera = () => {
const {formatMessage} = this.props.intl;
const options = {
quality: 0.7,
noData: true,
storageOptions: {
cameraRoll: true,
waitUntilSaved: true
},
permissionDenied: {
title: formatMessage({
id: 'mobile.android.camera_permission_denied_title',
defaultMessage: 'Camera access is required'
}),
text: formatMessage({
id: 'mobile.android.camera_permission_denied_description',
defaultMessage: 'To take photos and videos with your camera, please change your permission settings.'
}),
reTryTitle: formatMessage({
id: 'mobile.android.permission_denied_retry',
defaultMessage: 'Set Permission'
}),
okTitle: formatMessage({id: 'mobile.android.permission_denied_dismiss', defaultMessage: 'Dismiss'})
}
};
@@ -56,25 +38,9 @@ class AttachmentButton extends PureComponent {
};
attachFileFromLibrary = () => {
const {formatMessage} = this.props.intl;
const options = {
quality: 0.7,
noData: true,
permissionDenied: {
title: formatMessage({
id: 'mobile.android.photos_permission_denied_title',
defaultMessage: 'Photo library access is required'
}),
text: formatMessage({
id: 'mobile.android.photos_permission_denied_description',
defaultMessage: 'To upload images from your library, please change your permission settings.'
}),
reTryTitle: formatMessage({
id: 'mobile.android.permission_denied_retry',
defaultMessage: 'Set Permission'
}),
okTitle: formatMessage({id: 'mobile.android.permission_denied_dismiss', defaultMessage: 'Dismiss'})
}
noData: true
};
if (Platform.OS === 'ios') {
@@ -91,26 +57,10 @@ class AttachmentButton extends PureComponent {
};
attachVideoFromLibraryAndroid = () => {
const {formatMessage} = this.props.intl;
const options = {
quality: 0.7,
mediaType: 'video',
noData: true,
permissionDenied: {
title: formatMessage({
id: 'mobile.android.videos_permission_denied_title',
defaultMessage: 'Video library access is required'
}),
text: formatMessage({
id: 'mobile.android.videos_permission_denied_description',
defaultMessage: 'To upload videos from your library, please change your permission settings.'
}),
reTryTitle: formatMessage({
id: 'mobile.android.permission_denied_retry',
defaultMessage: 'Set Permission'
}),
okTitle: formatMessage({id: 'mobile.android.permission_denied_dismiss', defaultMessage: 'Dismiss'})
}
noData: true
};
ImagePicker.launchImageLibrary(options, (response) => {
@@ -120,7 +70,7 @@ class AttachmentButton extends PureComponent {
this.uploadFiles([response]);
});
};
}
uploadFiles = (images) => {
this.props.uploadFiles(images);
@@ -225,5 +175,3 @@ const style = StyleSheet.create({
justifyContent: 'center'
}
});
export default injectIntl(AttachmentButton);

View File

@@ -5,7 +5,7 @@ import {connect} from 'react-redux';
import {getUsersTyping} from 'mattermost-redux/selectors/entities/typing';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import Typing from './typing';

View File

@@ -6,15 +6,14 @@ import {connect} from 'react-redux';
import {createPost} from 'mattermost-redux/actions/posts';
import {userTyping} from 'mattermost-redux/actions/websocket';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {canUploadFilesOnMobile} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {addReactionToLatestPost} from 'app/actions/views/emoji';
import {handlePostDraftChanged, handlePostDraftSelectionChanged} from 'app/actions/views/channel';
import {handlePostDraftChanged} from 'app/actions/views/channel';
import {handleClearFiles, handleRemoveLastFile, handleUploadFiles} from 'app/actions/views/file_upload';
import {handleCommentDraftChanged, handleCommentDraftSelectionChanged} from 'app/actions/views/thread';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {handleCommentDraftChanged} from 'app/actions/views/thread';
import {getTheme} from 'app/selectors/preferences';
import {getCurrentChannelDraft, getThreadDraft} from 'app/selectors/views';
import PostTextbox from './post_textbox';
@@ -23,7 +22,6 @@ function mapStateToProps(state, ownProps) {
const currentDraft = ownProps.rootId ? getThreadDraft(state, ownProps.rootId) : getCurrentChannelDraft(state);
return {
channelId: getCurrentChannelId(state),
canUploadFiles: canUploadFilesOnMobile(state),
channelIsLoading: state.views.channel.loading,
currentUserId: getCurrentUserId(state),
@@ -44,9 +42,7 @@ function mapDispatchToProps(dispatch) {
handlePostDraftChanged,
handleRemoveLastFile,
handleUploadFiles,
userTyping,
handlePostDraftSelectionChanged,
handleCommentDraftSelectionChanged
userTyping
}, dispatch)
};
}

View File

@@ -39,9 +39,7 @@ class PostTextbox extends PureComponent {
handleClearFiles: PropTypes.func.isRequired,
handleRemoveLastFile: PropTypes.func.isRequired,
handleUploadFiles: PropTypes.func.isRequired,
userTyping: PropTypes.func.isRequired,
handlePostDraftSelectionChanged: PropTypes.func.isRequired,
handleCommentDraftSelectionChanged: PropTypes.func.isRequired
userTyping: PropTypes.func.isRequired
}).isRequired,
canUploadFiles: PropTypes.bool.isRequired,
channelId: PropTypes.string.isRequired,
@@ -58,14 +56,14 @@ class PostTextbox extends PureComponent {
static defaultProps = {
files: [],
onSelectionChange: () => true,
rootId: '',
value: ''
};
state = {
contentHeight: INITIAL_HEIGHT,
inputWidth: null,
keyboardType: 'default'
inputWidth: null
};
componentDidMount() {
@@ -190,19 +188,9 @@ class PostTextbox extends PureComponent {
}
// Shrink the input textbox since the layout events lag slightly
const nextState = {
this.setState({
contentHeight: INITIAL_HEIGHT
};
// Fixes the issue where Android predictive text would prepend suggestions to the post draft when messages
// are typed successively without blurring the input
let callback;
if (Platform.OS === 'android') {
nextState.keyboardType = 'email-address';
callback = () => this.setState({keyboardType: 'default'});
}
this.setState(nextState, callback);
});
};
handleUploadFiles = (images) => {
@@ -240,6 +228,12 @@ class PostTextbox extends PureComponent {
actions.userTyping(channelId, rootId);
};
handleSelectionChange = (event) => {
if (this.autocomplete) {
this.autocomplete.handleSelectionChange(event);
}
};
handleContentSizeChange = (event) => {
let contentHeight = event.nativeEvent.layout.height;
if (contentHeight < INITIAL_HEIGHT) {
@@ -314,17 +308,6 @@ class PostTextbox extends PureComponent {
return null;
}
handlePostDraftSelectionChanged = (event) => {
const cursorPosition = event.nativeEvent.selection.end;
if (this.props.rootId) {
this.props.actions.handleCommentDraftSelectionChanged(this.props.rootId, cursorPosition);
} else {
this.props.actions.handlePostDraftSelectionChanged(this.props.channelId, cursorPosition);
}
this.autocomplete.handleSelectionChange(event);
}
render() {
const {
canUploadFiles,
@@ -388,7 +371,7 @@ class PostTextbox extends PureComponent {
ref='input'
value={textValue}
onChangeText={this.handleTextChange}
onSelectionChange={this.handlePostDraftSelectionChanged}
onSelectionChange={this.handleSelectionChange}
placeholder={intl.formatMessage(placeholder)}
placeholderTextColor={changeOpacity('#000', 0.5)}
multiline={true}
@@ -398,7 +381,6 @@ class PostTextbox extends PureComponent {
style={[style.input, {height: textInputHeight}]}
onSubmitEditing={this.handleSubmit}
onLayout={this.handleInputSizeChange}
keyboardType={this.state.keyboardType}
/>
{this.renderSendButton()}
</View>

View File

@@ -4,7 +4,7 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import {getStatusesByIdsBatchedDebounced} from 'mattermost-redux/actions/users';
import {getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users';
@@ -20,7 +20,8 @@ function mapStateToProps(state, ownProps) {
return {
theme: ownProps.theme || getTheme(state),
status,
user
user,
...ownProps
};
}

View File

@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
import {Animated, Text, View} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
const DISABLED_OPACITY = 0.26;

View File

@@ -8,7 +8,7 @@ import {addReaction, getReactionsForPost, removeReaction} from 'mattermost-redux
import {makeGetReactionsForPost} from 'mattermost-redux/selectors/entities/posts';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import Reactions from './reactions';

View File

@@ -1,23 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getConnection} from 'app/selectors/device';
import RefreshList from './refresh_list';
function mapStateToProps(state) {
const networkOnline = getConnection(state);
let {refreshing} = state.views.channel;
if (!networkOnline) {
refreshing = false;
}
return {
refreshing
};
}
export default connect(mapStateToProps)(RefreshList);

View File

@@ -1,19 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import {RefreshControl} from 'react-native';
export default class RefreshList extends PureComponent {
static propTypes = {
...RefreshControl.propTypes
};
render() {
return (
<RefreshControl
{...this.props}
/>
);
}
}

View File

@@ -1,25 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {RequestStatus} from 'mattermost-redux/constants';
import {getConnection} from 'app/selectors/device';
import RetryBarIndicator from './retry_bar_indicator';
function mapStateToProps(state) {
const {websocket: websocketRequest} = state.requests.general;
const networkOnline = getConnection(state);
const webSocketOnline = websocketRequest.status === RequestStatus.SUCCESS;
let failed = state.views.channel.retryFailed && webSocketOnline;
if (!networkOnline) {
failed = false;
}
return {
failed
};
}
export default connect(mapStateToProps)(RetryBarIndicator);

View File

@@ -1,65 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Animated,
StyleSheet
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
const {View: AnimatedView} = Animated;
export default class RetryBarIndicator extends PureComponent {
static propTypes = {
failed: PropTypes.bool
};
state = {
retryMessageHeight: new Animated.Value(0)
};
componentWillReceiveProps(nextProps) {
if (this.props.failed !== nextProps.failed) {
this.toggleRetryMessage(nextProps.failed);
}
}
toggleRetryMessage = (show = true) => {
const value = show ? 38 : 0;
Animated.timing(this.state.retryMessageHeight, {
toValue: value,
duration: 350
}).start();
};
render() {
const {retryMessageHeight} = this.state;
const refreshIndicatorDimensions = {
height: retryMessageHeight
};
return (
<AnimatedView style={[style.refreshIndicator, refreshIndicatorDimensions]}>
<FormattedText
id='mobile.retry_message'
defaultMessage='Refreshing messages failed. Pull up to try again.'
style={{color: 'white', flex: 1, fontSize: 12}}
/>
</AnimatedView>
);
}
}
const style = StyleSheet.create({
refreshIndicator: {
alignItems: 'center',
backgroundColor: '#fb8000',
flexDirection: 'row',
paddingHorizontal: 10,
position: 'absolute',
top: 0,
overflow: 'hidden',
width: '100%'
}
});

View File

@@ -8,7 +8,7 @@ import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getCurrentLocale} from 'app/selectors/i18n';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import {removeProtocol} from 'app/utils/url';
import Root from './root';

View File

@@ -49,8 +49,8 @@ export default class SearchBarAndroid extends PureComponent {
static defaultProps = {
backArrowSize: 24,
deleteIconSize: 20,
searchIconSize: 24,
deleteIconSize: 16,
searchIconSize: 16,
blurOnSubmit: true,
placeholder: 'Search',
showCancelButton: true,
@@ -99,10 +99,6 @@ export default class SearchBarAndroid extends PureComponent {
});
};
onClearPress = () => {
this.onChangeText('');
};
onChangeText = (value) => {
this.props.onChangeText(value);
};
@@ -174,7 +170,7 @@ export default class SearchBarAndroid extends PureComponent {
{
backgroundColor: inputColor,
height: inputHeight,
paddingLeft: 7
paddingLeft: isFocused ? 0 : inputHeight * 0.25
}
]}
>
@@ -216,14 +212,13 @@ export default class SearchBarAndroid extends PureComponent {
style={[
styles.searchBarInput,
inputNoBackground,
{height: this.props.inputHeight},
isFocused ? {} : styles.searchBarBlurredInput
{height: this.props.inputHeight}
]}
/>
{isFocused && value ?
<TouchableWithoutFeedback onPress={this.onClearPress}>
<TouchableWithoutFeedback onPress={() => this.onChangeText('')}>
<Icon
style={[{paddingRight: 7}]}
style={[{paddingRight: (inputHeight * 0.2)}]}
name='close'
size={deleteIconSize}
color={tintColorDelete || placeholderTextColor}
@@ -241,7 +236,8 @@ const styles = StyleSheet.create({
backgroundColor: 'grey',
flexDirection: 'row',
justifyContent: 'flex-start',
alignItems: 'center'
alignItems: 'center',
padding: 5
},
searchBar: {
flex: 1,
@@ -252,10 +248,7 @@ const styles = StyleSheet.create({
flex: 1,
fontWeight: 'normal',
textAlignVertical: 'center',
fontSize: 15,
padding: 0,
includeFontPadding: false
},
searchBarBlurredInput: {
padding: 0
}
});

View File

@@ -101,10 +101,10 @@ export default class SearchBarIos extends PureComponent {
<Search
{...this.props}
ref='search'
placeholderCollapsedMargin={33}
placeholderExpandedMargin={33}
searchIconCollapsedMargin={10}
searchIconExpandedMargin={10}
placeholderCollapsedMargin={25}
placeholderExpandedMargin={25}
searchIconCollapsedMargin={15}
searchIconExpandedMargin={15}
shadowVisible={false}
onCancel={this.onCancel}
onChangeText={this.onChangeText}

View File

@@ -15,12 +15,10 @@ import {
StyleSheet,
View
} from 'react-native';
import EvilIcon from 'react-native-vector-icons/EvilIcons';
import IonIcon from 'react-native-vector-icons/Ionicons';
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
const AnimatedIonIcon = Animated.createAnimatedComponent(IonIcon);
const AnimatedEvilcon = Animated.createAnimatedComponent(EvilIcon);
const AnimatedIcon = Animated.createAnimatedComponent(IonIcon);
const containerHeight = 40;
const middleHeight = 20;
@@ -330,6 +328,11 @@ export default class Search extends Component {
};
render() {
let iconSize = 16;
if (this.props.inputStyle && this.props.inputStyle.fontSize) {
iconSize = this.props.inputStyle.fontSize + 2;
}
return (
<Animated.View
ref='searchContainer'
@@ -388,16 +391,16 @@ export default class Search extends Component {
>
{this.props.iconSearch}
</Animated.View> :
<AnimatedEvilcon
name='search'
size={24}
<AnimatedIcon
name='ios-search-outline'
size={iconSize}
style={[
styles.iconSearch,
styles.iconSearchDefault,
this.props.tintColorSearch && {color: this.props.tintColorSearch},
{
left: this.iconSearchAnimated,
top: middleHeight - 10
top: middleHeight - (iconSize / 2)
}
]}
/>
@@ -414,10 +417,10 @@ export default class Search extends Component {
>
{this.props.iconDelete}
</Animated.View> :
<View style={[styles.iconDelete, this.props.inputHeight && {height: this.props.inputHeight}]}>
<AnimatedIonIcon
<View style={[styles.iconDelete, this.props.inputHeight && {height: this.props.inputHeight, width: iconSize + 5}]}>
<AnimatedIcon
name='ios-close-circle'
size={17}
size={iconSize}
style={[
styles.iconDeleteDefault,
this.props.tintColorDelete && {color: this.props.tintColorDelete},
@@ -465,13 +468,13 @@ const styles = StyleSheet.create({
},
input: {
height: containerHeight - 10,
paddingTop: 7,
paddingTop: 5,
paddingBottom: 5,
paddingRight: 32,
paddingRight: 20,
borderColor: '#444',
backgroundColor: '#f7f7f7',
borderRadius: 5,
fontSize: 15
fontSize: 13
},
iconSearch: {
flex: 1,
@@ -481,13 +484,10 @@ const styles = StyleSheet.create({
color: 'grey'
},
iconDelete: {
alignItems: 'flex-start',
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
paddingLeft: 1,
paddingTop: 3,
right: 65,
width: 25
right: 70
},
iconDeleteDefault: {
color: 'grey'

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