RN-97 Changed navigation from NavigationExperimental to ReactNative Navigation (#540)

* Add react-navigator dependency

* Add react-native-navigation dependency

* react-native-navigation, ios and android config

* update react-native-navigation

* New navigation and code cleanup

* Set ScreenBackgroundColor

* Fix channel drawer event issue

* Applied RNPN non-working popInitial notification

* Android navbar and back button

* packages and notices

* MM-redux update

* Fix draft on team switch

* Remove custom NavBar
This commit is contained in:
enahum 2017-05-16 11:19:46 -04:00 committed by GitHub
parent 74e93cae68
commit 248dafbe0e
135 changed files with 2824 additions and 4656 deletions

View file

@ -8,49 +8,6 @@ This document includes a list of open source components used in Mattermost Mobil
--------
## fbjs
This product contains a modified portion of 'fbjs', a collection of JavaScript utilities by Facebook, Inc.
* HOMEPAGE:
* https://github.com/facebook/fbjs
* LICENSE:
BSD License
For fbjs software
Copyright (c) 2013-present, Facebook, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name Facebook nor the names of its contributors may be used to
endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---
## react-native-search-bar
This product contains a modified portion of 'react-native-search-bar', a high-quality iOS native search bar for react native by Zhao Han.
@ -117,49 +74,6 @@ SOFTWARE.
---
## harmony-reflect
[Note: the software referenced below is made available under two licenses. Mattermost, Inc. has elected to license the software pursuant to the Apache License, Version 2.0.]
This product contains 'harmony-reflect', a shim for ECMAScript 6 Reflect and Proxy objects.
* HOMEPAGE:
* https://github.com/tvcutsem/harmony-reflect
* LICENSE:
Copyright (C) 2011-2014 Software Languages Lab, Vrije Universiteit Brussel
Copyright (C) 2015 Tom Van Cutsem
This code is dual-licensed under both the Apache License and the MPL
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Version: MPL 1.1
The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.
---
## intl
This product contains 'Intl.js', to bring long overdue localization methods to ECMAScript implementations by Andy Earnshaw.
@ -237,39 +151,6 @@ the property of their respective owners.
---
## isomorphic-fetch
This product contains 'isomorphic-fetch', to provide the fetch function for node by Matt Andrews.
* HOMEPAGE:
* https://github.com/matthew-andrews/isomorphic-fetch
* LICENSE:
The MIT License (MIT)
Copyright (c) 2015 Matt Andrews
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react
This product contains 'react', a declarative, efficient, and
@ -769,39 +650,6 @@ SOFTWARE.
---
## react-native-message-bar
This product contains 'react-native-message-bar', a library for displaying a message bar notification component at the top of the screen for React Native (Android and iOS) projects.
* HOMEPAGE
* https://github.com/KBLNY/react-native-message-bar
* LICENSE:
The MIT License (MIT)
Copyright (c) 2016 KBLNY
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-device-info
This product contains 'react-native-device-info', library to provide a device information to react native on both iOS and Android.
@ -889,37 +737,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
---
## redux-action-buffer
A middleware for redux that buffers all actions into a queue until a breaker condition is met, at which point the queue is released (i.e. actions are triggered).
* HOMEPAGE
* https://github.com/rt2zz/redux-action-buffer
* LICENSE
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## redux-persist
Persist and rehydrate a redux store.
@ -1024,3 +841,35 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## react-native-navigation
This product contains 'react-native-navigation', App-wide support for 100% native navigation with an easy cross-platform interface.
* HOMEPAGE:
* https://github.com/wix/react-native-navigation
* LICENSE:
The MIT License (MIT)
Copyright (c) Wix.com
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -84,8 +84,8 @@ def enableSeparateBuildPerCPUArchitecture = false
def enableProguardInReleaseBuilds = false
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
compileSdkVersion 25
buildToolsVersion "25.0.1"
defaultConfig {
applicationId "com.mattermost.react.native"
@ -144,8 +144,11 @@ android {
}
dependencies {
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:25.0.1"
compile "com.facebook.react:react-native:+" // From node_modules
compile project(':react-native-navigation')
compile project(':react-native-image-picker')
compile project(':react-native-smart-splashscreen')
compile project(':react-native-orientation')
compile project(':react-native-bottom-sheet')
compile project(':react-native-push-notification')
@ -157,9 +160,6 @@ dependencies {
compile project(':react-native-linear-gradient')
compile project(':react-native-vector-icons')
compile project(':react-native-svg')
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:23.0.1"
compile "com.facebook.react:react-native:+" // From node_modules
}
// Run this once to be able to run the application with BUCK

View file

@ -34,8 +34,6 @@
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:launchMode="singleInstance"
android:taskAffinity=""
>
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

@ -1,49 +1,67 @@
package com.mattermost;
import com.facebook.react.ReactActivity;
import com.github.yamill.orientation.OrientationPackage;
import com.psykar.cookiemanager.CookieManagerPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import com.reactnativecomponent.splashscreen.RCTSplashScreen;
public class MainActivity extends ReactActivity {
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 {
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
// protected String getMainComponentName() {
// return "Mattermost";
// }
@Override
protected void onCreate(Bundle savedInstanceState) {
RCTSplashScreen.openSplashScreen(this);
super.onCreate(savedInstanceState);
public LinearLayout createSplashLayout() {
wr_activity = new WeakReference<>(this);
final int drawableId = getImageId();
Context context = getActivity();
imageView = new ImageView(context);
imageView.setImageResource(drawableId);
LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
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;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Intent intent = new Intent("onConfigurationChanged");
intent.putExtra("newConfig", newConfig);
this.sendBroadcast(intent);
}
/**
* When the back button is pressed and the app is closed it will
* be sent to the background instead of exiting fixing the newIntent
* problem where the splash screen wasn't being removed
*/
@Override
public void invokeDefaultOnBackPressed() {
Intent setIntent = new Intent(Intent.ACTION_MAIN);
setIntent.addCategory(Intent.CATEGORY_HOME);
setIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(setIntent);
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

@ -2,8 +2,15 @@ package com.mattermost;
import android.app.Application;
import android.util.Log;
import android.support.annotation.NonNull;
import com.facebook.react.ReactApplication;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import com.imagepicker.ImagePickerPackage;
import com.gnet.bottomsheet.RNBottomSheetPackage;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
@ -11,32 +18,47 @@ import com.psykar.cookiemanager.CookieManagerPackage;
import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.horcrux.svg.SvgPackage;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;
import com.BV.LinearGradient.LinearGradientPackage;
import com.reactnativecomponent.splashscreen.RCTSplashScreenPackage;
import com.github.yamill.orientation.OrientationPackage;
import com.reactnativenavigation.NavigationApplication;
import java.util.Arrays;
import java.util.List;
public class MainApplication extends Application implements ReactApplication {
// public class MainApplication extends Application implements ReactApplication {
public class MainApplication extends NavigationApplication {
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
// private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
// @Override
// protected boolean getUseDeveloperSupport() {
// return BuildConfig.DEBUG;
// }
//
// @Override
// protected List<ReactPackage> getPackages() {
// return Arrays.<ReactPackage>asList(
// new MainReactPackage()
// );
// }
// };
//
// @Override
// public ReactNativeHost getReactNativeHost() {
// return mReactNativeHost;
// }
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
@Override
public boolean isDebug() {
return BuildConfig.DEBUG;
}
@NonNull
@Override
public List<ReactPackage> createAdditionalReactPackages() {
// Add the packages you require here.
// No need to add RnnPackage and MainReactPackage
return Arrays.<ReactPackage>asList(
new ImagePickerPackage(),
new RCTSplashScreenPackage(),
new RNBottomSheetPackage(),
new RNDeviceInfo(),
new CookieManagerPackage(),
@ -45,13 +67,7 @@ public class MainApplication extends Application implements ReactApplication {
new SvgPackage(),
new LinearGradientPackage(),
new OrientationPackage()
);
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
);
}
@Override

View file

@ -1,8 +1,8 @@
rootProject.name = 'Mattermost'
include ':react-native-navigation'
project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/android/app/')
include ':react-native-image-picker'
project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android')
include ':react-native-smart-splashscreen'
project(':react-native-smart-splashscreen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-smart-splash-screen/android')
include ':react-native-bottom-sheet'
project(':react-native-bottom-sheet').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-bottom-sheet/android')
include ':react-native-device-info'

View file

@ -1,298 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {NavigationTypes} from 'app/constants';
import Routes from 'app/navigation/routes';
import {General} from 'mattermost-redux/constants';
import {selectPost} from 'mattermost-redux/actions/posts';
export function closeDrawers() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_CLOSE_DRAWERS
}, getState);
};
}
export function closeModal() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_CLOSE_MODAL
}, getState);
};
}
export function goBack() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_POP
}, getState);
};
}
export function goToAbout() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.About
}, getState);
};
}
export function goToAccountNotifications() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.AccountNotifications
}, getState);
};
}
export function goToAccountSettings() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.AccountSettings
}, getState);
};
}
export function goToChannelAddMembers() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.ChannelAddMembers
}, getState);
};
}
export function goToChannelInfo() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.ChannelInfo
}, getState);
};
}
export function goToChannelMembers() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.ChannelMembers
}, getState);
};
}
export function goToCreateChannel(channelType) {
return async (dispatch, getState) => {
closeDrawers()(dispatch, getState);
let type;
let route;
switch (channelType) {
case General.OPEN_CHANNEL:
type = NavigationTypes.NAVIGATION_PUSH;
route = Routes.CreatePublicChannel;
break;
case General.PRIVATE_CHANNEL:
type = NavigationTypes.NAVIGATION_MODAL;
route = Routes.CreatePrivateChannel;
break;
default:
return;
}
dispatch({
type,
route: {
...route,
props: {
channelType
}
}
}, getState);
};
}
export function goToImagePreviewModal(post, fileId) {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_MODAL,
route: {
...Routes.ImagePreview,
props: {
post,
fileId
}
}
}, getState);
};
}
export function goToLoadTeam() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.LoadTeam
}, getState);
};
}
export function goToLogin() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.Login
}, getState);
};
}
export function goToLoginOptions() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.LoginOptions
}, getState);
};
}
export function goToMfa() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.Mfa
}, getState);
};
}
export function goToSaml() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.Saml
}, getState);
};
}
export function goToSelectTeam() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.SelectTeam
}, getState);
};
}
export function goToThread(channelId, rootId) {
return async (dispatch, getState) => {
selectPost(rootId)(dispatch, getState);
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: {
...Routes.Thread,
key: Routes.Thread.key + rootId,
props: {
channelId,
rootId
}
}
}, getState);
};
}
export function goToUserProfile(userId) {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: {
...Routes.UserProfile,
props: {
userId
}
}
}, getState);
};
}
export function openChannelDrawer() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_OPEN_LEFT_DRAWER,
route: Routes.ChannelDrawer
}, getState);
};
}
export function openEditPostModal(post) {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_MODAL,
route: {
...Routes.EditPost,
props: {
post
}
}
}, getState);
};
}
export function openSettingsModal() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_MODAL,
route: Routes.Settings
}, getState);
};
}
export function requestCloseModal() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_REQUEST_CLOSE_MODAL
}, getState);
};
}
export function renderDrawer() {
return async (dispatch, getState) => {
dispatch({type: NavigationTypes.NAVIGATION_RENDER_LEFT_DRAWER, data: true}, getState);
};
}
export function unrenderDrawer() {
return async (dispatch, getState) => {
dispatch({type: NavigationTypes.NAVIGATION_RENDER_LEFT_DRAWER, data: false}, getState);
};
}
export function showOptionsModal(options) {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_MODAL,
route: {
...Routes.OptionsModal,
props: options
}
}, getState);
};
}
export function showMoreChannelsModal() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_MODAL,
route: Routes.MoreChannels
}, getState);
};
}
export function showDirectMessagesModal() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_MODAL,
route: Routes.MoreDirectMessages
}, getState);
};
}

View file

@ -26,6 +26,10 @@ export function handleUpdateUserNotifyProps(notifyProps) {
}
const props = {...otherProps, email};
updateMe({notify_props: props})(dispatch, getState);
try {
await updateMe({notify_props: props})(dispatch, getState);
} catch (error) {
// do nothing
}
};
}

View file

@ -288,19 +288,6 @@ export function closeGMChannel(channel) {
};
}
export function closeDirectChannel(channel) {
return async (dispatch, getState) => {
switch (channel.type) {
case General.DM_CHANNEL:
return closeDMChannel(channel)(dispatch, getState);
case General.GM_CHANNEL:
return closeGMChannel(channel)(dispatch, getState);
}
return null;
};
}
export function markFavorite(channelId) {
return async (dispatch, getState) => {
const {currentUserId} = getState().entities.users;

View file

@ -1,38 +1,14 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {NavigationTypes, ViewTypes} from 'app/constants';
import Routes from 'app/navigation/routes';
import {ViewTypes} from 'app/constants';
export function goToChannelView() {
export function initialize() {
return async (dispatch, getState) => {
const state = getState();
if (state.views.login.loginId) {
setTimeout(() => {
dispatch({
type: NavigationTypes.NAVIGATION_REPLACE_AT_ROOT,
route: Routes.ChannelView
type: ViewTypes.APPLICATION_INITIALIZED
}, getState);
dispatch({
type: NavigationTypes.NAVIGATION_CLOSE_MODAL
}, getState);
setTimeout(() => {
dispatch({
type: ViewTypes.APPLICATION_INITIALIZED
}, getState);
}, 400);
} else {
dispatch({
type: NavigationTypes.NAVIGATION_RESET,
routes: [Routes.ChannelView]
}, getState);
setTimeout(() => {
dispatch({
type: ViewTypes.APPLICATION_INITIALIZED
}, getState);
}, 400);
}
}, 400);
};
}

View file

@ -1,30 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {ViewTypes} from 'app/constants';
export function openModal(title, options) {
return async (dispatch, getState) => {
dispatch({
type: ViewTypes.OPTIONS_MODAL_CHANGED,
data: {
title,
options,
visible: true
}
}, getState);
};
}
export function closeModal() {
return async (dispatch, getState) => {
dispatch({
type: ViewTypes.OPTIONS_MODAL_CHANGED,
data: {
title: '',
options: [],
visible: false
}
}, getState);
};
}

View file

@ -1,39 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {batchActions} from 'redux-batched-actions';
import {NavigationTypes} from 'app/constants';
import Routes from 'app/navigation/routes';
export function goToRecentMentions() {
return async (dispatch, getState) => {
dispatch(batchActions([{
type: NavigationTypes.NAVIGATION_CLOSE_DRAWERS
}, {
type: NavigationTypes.NAVIGATION_PUSH,
route: {
...Routes.Search,
props: {
searchType: 'recent_mentions'
}
}
}]), getState);
};
}
export function goToFlaggedPosts() {
return async (dispatch, getState) => {
dispatch(batchActions([{
type: NavigationTypes.NAVIGATION_CLOSE_DRAWERS
}, {
type: NavigationTypes.NAVIGATION_PUSH,
route: {
...Routes.Search,
props: {
searchType: 'flagged_posts'
}
}
}]), getState);
};
}

View file

@ -1,28 +1,17 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {NavigationTypes, ViewTypes} from 'app/constants';
import Routes from 'app/navigation/routes';
import {ViewTypes} from 'app/constants';
import {
handleSelectChannel,
loadChannelsIfNecessary,
loadProfilesAndTeamMembersForDMSidebar
} from 'app/actions/views/channel';
import {goToChannelView} from 'app/actions/views/load_team';
import {handleTeamChange, selectFirstAvailableTeam} from 'app/actions/views/select_team';
import {getClientConfig, getLicenseConfig, setServerVersion} from 'mattermost-redux/actions/general';
import {markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
export function goToSelectServer() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_MODAL,
route: Routes.SelectServer
}, getState);
};
}
export function loadConfigAndLicense(serverVersion) {
return async (dispatch, getState) => {
getClientConfig()(dispatch, getState);
@ -54,26 +43,24 @@ export function goToNotification(notification) {
let teamId = data.team_id || currentTeamId;
if (teamId) {
await handleTeamChange(teams[teamId])(dispatch, getState);
handleTeamChange(teams[teamId])(dispatch, getState);
await loadChannelsIfNecessary(teamId)(dispatch, getState);
} else {
await selectFirstAvailableTeam()(dispatch, getState);
teamId = currentTeamId;
}
viewChannel(teamId, channelId)(dispatch, getState);
await loadProfilesAndTeamMembersForDMSidebar(teamId)(dispatch, getState);
viewChannel(channelId)(dispatch, getState);
loadProfilesAndTeamMembersForDMSidebar(teamId)(dispatch, getState);
// when the notification is tapped go to the channel view before selecting the channel to prevent
// weird behavior
goToChannelView()(dispatch, getState);
await handleSelectChannel(channelId)(dispatch, getState);
markChannelAsRead(teamId, channelId)(dispatch, getState);
handleSelectChannel(channelId)(dispatch, getState);
markChannelAsRead(teamId, channelId)(dispatch, getState).then(() => true).catch(() => true);
};
}
export default {
goToSelectServer,
loadConfigAndLicense,
queueNotification,
clearNotification,

View file

@ -2,7 +2,6 @@
// See License.txt for license information.
import {ViewTypes} from 'app/constants';
import {goToLogin, goToLoginOptions} from 'app/actions/navigation';
export function handleServerUrlChanged(serverUrl) {
return async (dispatch, getState) => {
@ -13,26 +12,6 @@ export function handleServerUrlChanged(serverUrl) {
};
}
export function handleLoginOptions() {
return async (dispatch, getState) => {
const {config, license} = getState().entities.general;
const samlEnabled = config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true';
let options = 0;
if (samlEnabled) {
options += 1;
}
if (options) {
await goToLoginOptions()(dispatch, getState);
} else {
await goToLogin()(dispatch, getState);
}
};
}
export default {
handleServerUrlChanged,
handleLoginOptions
handleServerUrlChanged
};

View file

@ -2,11 +2,9 @@
// See License.txt for license information.
import {makeDirectChannel} from 'app/actions/views/more_dms';
import {NavigationTypes} from 'app/constants';
export function handleSendMessage(otherUserId) {
return async (dispatch, getState) => {
await makeDirectChannel(otherUserId)(dispatch, getState);
dispatch({type: NavigationTypes.NAVIGATION_POP_TO_INDEX, index: 0}, getState);
};
}

View file

@ -1,125 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {connect} from 'react-redux';
import {
TouchableOpacity,
View
} from 'react-native';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import {getTheme} from 'app/selectors/preferences';
import {changeOpacity} from 'app/utils/theme';
class ActionButton extends PureComponent {
static propTypes = {
actionEventName: PropTypes.string.isRequired,
emitter: PropTypes.func.isRequired,
enabled: PropTypes.bool,
enableEventName: PropTypes.string,
labelDefaultMessage: PropTypes.string.isRequired,
labelId: PropTypes.string.isRequired,
loadingEventName: PropTypes.string.isRequired,
theme: PropTypes.object
};
static defaultProps = {
theme: {},
enabled: true
};
constructor(props) {
super(props);
this.state = {
enabled: props.enabled,
loading: false
};
}
componentWillMount() {
EventEmitter.on(this.props.enableEventName, this.handleEnableEvent);
EventEmitter.on(this.props.loadingEventName, this.handleLoadingEvent);
}
componentWillUnmount() {
EventEmitter.off(this.props.enableEventName, this.handleEnableEvent);
EventEmitter.off(this.props.loadingEventName, this.handleLoadingEvent);
}
handleEnableEvent = (enabled) => {
this.setState({enabled});
};
handleLoadingEvent = (loading) => {
this.setState({loading});
};
onPress = () => {
if (this.state.enabled) {
this.props.emitter(this.props.actionEventName);
}
};
render() {
const {labelDefaultMessage, labelId, theme} = this.props;
const {enabled, loading} = this.state;
let color = changeOpacity(theme.sidebarHeaderTextColor, 0.4);
let contents;
if (enabled) {
color = theme.sidebarHeaderTextColor;
contents = (
<View style={{flex: 1, flexDirection: 'column', justifyContent: 'center', alignItems: 'center'}}>
<TouchableOpacity
onPress={this.onPress}
style={{paddingHorizontal: 15}}
>
<FormattedText
id={labelId}
defaultMessage={labelDefaultMessage}
style={{color}}
/>
</TouchableOpacity>
</View>
);
} else {
contents = (
<View style={{flex: 1, flexDirection: 'column', justifyContent: 'center', alignItems: 'center', paddingHorizontal: 15}}>
<FormattedText
id={labelId}
defaultMessage={labelDefaultMessage}
style={{color}}
/>
</View>
);
}
if (loading) {
return (
<Loading
color={color}
size='small'
style={{
alignItems: 'center',
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
paddingHorizontal: 20}}
/>
);
}
return contents;
}
}
function mapStateToProps(state) {
return {
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(ActionButton);

View file

@ -38,6 +38,7 @@ export default class AtMention extends Component {
static defaultProps = {
autocompleteUsersInCurrentChannel: {},
defaultChannel: {},
postDraft: ''
};

View file

@ -17,7 +17,7 @@ function mapStateToProps(state, ownProps) {
let postDraft;
if (ownProps.rootId.length) {
postDraft = state.views.thread.drafts[ownProps.rootId].draft;
} else {
} else if (currentChannelId) {
postDraft = state.views.channel.drafts[currentChannelId].draft;
}

View file

@ -18,7 +18,7 @@ function mapStateToProps(state, ownProps) {
let postDraft;
if (ownProps.rootId.length) {
postDraft = state.views.thread.drafts[ownProps.rootId].draft;
} else {
} else if (currentChannelId) {
postDraft = state.views.channel.drafts[currentChannelId].draft;
}

View file

@ -0,0 +1,190 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {BackAndroid, InteractionManager, Keyboard} from 'react-native';
import Drawer from 'app/components/drawer';
import ChannelDrawerList from 'app/components/channel_drawer_list';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
export default class ChannelDrawer extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
handleSelectChannel: PropTypes.func.isRequired,
viewChannel: PropTypes.func.isRequired,
markChannelAsRead: PropTypes.func.isRequired,
setChannelLoading: PropTypes.func.isRequired
}).isRequired,
blurPostTextBox: PropTypes.func.isRequired,
navigator: PropTypes.object,
children: PropTypes.node,
currentTeam: PropTypes.object,
currentChannel: PropTypes.object,
channels: PropTypes.object,
channelMembers: PropTypes.object,
theme: PropTypes.object.isRequired
};
static defaultProps = {
currentTeam: {},
currentChannel: {}
};
state = {
openDrawer: false
};
componentDidMount() {
EventEmitter.on('open_channel_drawer', this.openChannelDrawer);
EventEmitter.on('close_channel_drawer', this.closeChannelDrawer);
BackAndroid.addEventListener('hardwareBackPress', this.handleAndroidBack);
}
componentWillUnmount() {
EventEmitter.off('open_channel_drawer', this.openChannelDrawer);
EventEmitter.off('close_channel_drawer', this.closeChannelDrawer);
BackAndroid.removeEventListener('hardwareBackPress', this.handleAndroidBack);
}
handleAndroidBack = () => {
if (this.state.openDrawer) {
this.setState({openDrawer: false});
return true;
}
return false;
};
closeChannelDrawer = () => {
this.setState({openDrawer: false});
};
handleDrawerClose = () => {
setTimeout(() => {
InteractionManager.clearInteractionHandle(this.closeLeftHandle);
});
};
handleDrawerCloseStart = () => {
this.closeLeftHandle = InteractionManager.createInteractionHandle();
};
handleDrawerOpen = () => {
this.setState({openDrawer: true});
Keyboard.dismiss();
setTimeout(() => {
InteractionManager.clearInteractionHandle(this.openLeftHandle);
});
};
handleDrawerOpenStart = () => {
this.openLeftHandle = InteractionManager.createInteractionHandle();
};
handleDrawerTween = (ratio) => {
const opacity = (ratio / 2);
EventEmitter.emit('drawer_opacity', opacity);
return {
mainOverlay: {
backgroundColor: this.props.theme.centerChannelBg,
opacity
},
drawerOverlay: {
backgroundColor: ratio ? '#000' : '#FFF',
opacity: ratio ? (1 - ratio) / 2 : 1
}
};
};
openChannelDrawer = () => {
this.props.blurPostTextBox();
this.setState({openDrawer: true});
};
selectChannel = (id) => {
const {
actions,
currentChannel
} = this.props;
const {
handleSelectChannel,
markChannelAsRead,
setChannelLoading,
viewChannel
} = actions;
markChannelAsRead(id, currentChannel.id);
setChannelLoading();
viewChannel(id, currentChannel.id);
this.closeChannelDrawer();
InteractionManager.runAfterInteractions(() => {
handleSelectChannel(id);
});
};
render() {
const {
children,
currentChannel,
currentTeam,
channels,
channelMembers,
navigator,
theme
} = this.props;
return (
<Drawer
open={this.state.openDrawer}
onOpenStart={this.handleDrawerOpenStart}
onOpen={this.handleDrawerOpen}
onCloseStart={this.handleDrawerCloseStart}
onClose={this.handleDrawerClose}
captureGestures='open'
type='static'
acceptTap={true}
disabled={false}
content={
<ChannelDrawerList
currentTeam={currentTeam}
currentChannel={currentChannel}
channels={channels}
channelMembers={channelMembers}
theme={theme}
onSelectChannel={this.selectChannel}
navigator={navigator}
/>
}
tapToClose={true}
openDrawerOffset={40}
onRequestClose={this.closeChannelDrawer}
panOpenMask={0.2}
panCloseMask={40}
panThreshold={0.25}
acceptPan={true}
negotiatePan={true}
useInteractionManager={false}
tweenHandler={this.handleDrawerTween}
elevation={-5}
styles={{
main: {
shadowColor: '#000000',
shadowOpacity: 0.4,
shadowRadius: 12,
shadowOffset: {
width: -4,
height: 0
}
}
}}
>
{children}
</Drawer>
);
}
}

View file

@ -4,7 +4,6 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {closeDrawers} from 'app/actions/navigation';
import {handleSelectChannel, setChannelLoading} from 'app/actions/views/channel';
import {getChannelsByCategory, getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
@ -15,8 +14,9 @@ import {viewChannel, markChannelAsRead} from 'mattermost-redux/actions/channels'
import ChannelDrawer from './channel_drawer.js';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
return {
...ownProps,
currentTeam: getCurrentTeam(state),
currentChannel: getCurrentChannel(state),
channels: getChannelsByCategory(state),
@ -28,7 +28,6 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
closeDrawers,
handleSelectChannel,
viewChannel,
markChannelAsRead,

View file

@ -26,24 +26,13 @@ import UnreadIndicator from './unread_indicator';
class ChannelDrawerList extends Component {
static propTypes = {
actions: PropTypes.shape({
closeDirectChannel: PropTypes.func.isRequired,
closeOptionsModal: PropTypes.func.isRequired,
goToCreateChannel: PropTypes.func.isRequired,
leaveChannel: PropTypes.func.isRequired,
markFavorite: PropTypes.func.isRequired,
openSettingsModal: React.PropTypes.func.isRequired,
unmarkFavorite: PropTypes.func.isRequired,
showOptionsModal: PropTypes.func.isRequired,
showDirectMessagesModal: PropTypes.func.isRequired,
showMoreChannelsModal: PropTypes.func.isRequired
}).isRequired,
canCreatePrivateChannels: PropTypes.bool.isRequired,
channels: PropTypes.object.isRequired,
channelMembers: PropTypes.object,
currentTeam: PropTypes.object.isRequired,
currentChannel: PropTypes.object,
intl: intlShape.isRequired,
navigator: PropTypes.object,
onSelectChannel: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired
};
@ -64,6 +53,10 @@ class ChannelDrawerList extends Component {
rowHasChanged: (a, b) => a !== b
}).cloneWithRows(this.buildData(props))
};
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).
then((source) => {
this.closeButton = source;
});
}
shouldComponentUpdate(nextProps, nextState) {
@ -178,7 +171,25 @@ class ChannelDrawerList extends Component {
};
createPrivateChannel = () => {
this.props.actions.goToCreateChannel(General.PRIVATE_CHANNEL);
const {intl, navigator, theme} = this.props;
navigator.showModal({
screen: 'CreateChannel',
animationType: 'slide-up',
title: intl.formatMessage({id: 'mobile.create_channel.private', defaultMessage: 'New Private Channel'}),
backButtonTitle: '',
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
},
passProps: {
channelType: General.PRIVATE_CHANNEL,
closeButton: this.closeButton
}
});
};
buildData = (props) => {
@ -206,7 +217,7 @@ class ChannelDrawerList extends Component {
}
data.push(
this.renderTitle(styles, 'sidebar.channels', 'CHANNELS', this.props.actions.showMoreChannelsModal, publicChannels.length > 0),
this.renderTitle(styles, 'sidebar.channels', 'CHANNELS', this.showMoreChannelsModal, publicChannels.length > 0),
...publicChannels
);
@ -220,7 +231,7 @@ class ChannelDrawerList extends Component {
);
data.push(
this.renderTitle(styles, 'sidebar.direct', 'DIRECT MESSAGES', this.props.actions.showDirectMessagesModal, directAndGroupChannels.length > 0),
this.renderTitle(styles, 'sidebar.direct', 'DIRECT MESSAGES', this.showDirectMessagesModal, directAndGroupChannels.length > 0),
...directAndGroupChannels
);
@ -231,6 +242,76 @@ class ChannelDrawerList extends Component {
return data;
};
openSettingsModal = () => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
screen: 'Settings',
title: intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}),
animationType: 'slide-up',
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
},
navigatorButtons: {
leftButtons: [{
id: 'close-settings',
icon: this.closeButton
}]
}
});
};
showDirectMessagesModal = () => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
screen: 'MoreDirectMessages',
title: intl.formatMessage({id: 'more_direct_channels.title', defaultMessage: 'Direct Messages'}),
animationType: 'slide-up',
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
},
navigatorButtons: {
leftButtons: [{
id: 'close-dms',
icon: this.closeButton
}]
}
});
};
showMoreChannelsModal = () => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
screen: 'MoreChannels',
animationType: 'slide-up',
title: intl.formatMessage({id: 'more_channels.title', defaultMessage: 'More Channels'}),
backButtonTitle: '',
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
},
passProps: {
channelType: General.PRIVATE_CHANNEL,
closeButton: this.closeButton
}
});
};
renderSectionAction = (styles, action) => {
const {theme} = this.props;
return (
@ -293,7 +374,7 @@ class ChannelDrawerList extends Component {
const settings = (
<TouchableHighlight
style={styles.settingsContainer}
onPress={() => preventDoubleTap(this.props.actions.openSettingsModal)}
onPress={() => preventDoubleTap(this.openSettingsModal)}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<AwesomeIcon

View file

@ -1,25 +1,8 @@
// 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 {
goToCreateChannel,
openSettingsModal,
requestCloseModal,
showMoreChannelsModal,
showDirectMessagesModal,
showOptionsModal
} from 'app/actions/navigation';
import {
closeDirectChannel,
leaveChannel,
markFavorite,
unmarkFavorite
} from 'app/actions/views/channel';
import {General} from 'mattermost-redux/constants';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {showCreateOption} from 'mattermost-redux/utils/channel_utils';
@ -37,21 +20,4 @@ function mapStateToProps(state, ownProps) {
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
closeDirectChannel,
goToCreateChannel,
leaveChannel,
markFavorite,
openSettingsModal,
unmarkFavorite,
showOptionsModal,
showDirectMessagesModal,
showMoreChannelsModal,
closeOptionsModal: requestCloseModal
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChannelDrawerList);
export default connect(mapStateToProps)(ChannelDrawerList);

View file

@ -24,6 +24,21 @@ function ChannelListRow(props) {
const {id, displayName, purpose, onPress, theme} = props;
const style = getStyleFromTheme(theme);
let purposeComponent;
if (purpose) {
purposeComponent = (
<View style={{flexShrink: 1, flexDirection: 'column', flexWrap: 'wrap'}}>
<Text
style={style.purpose}
ellipsizeMode='tail'
numberOfLines={1}
>
{purpose}
</Text>
</View>
);
}
const RowComponent = (
<View style={style.container}>
<View style={style.titleContainer}>
@ -54,15 +69,7 @@ function ChannelListRow(props) {
</View>
</View>
</View>
<View style={{flexShrink: 1, flexDirection: 'column', flexWrap: 'wrap'}}>
<Text
style={style.purpose}
ellipsizeMode='tail'
numberOfLines={1}
>
{purpose}
</Text>
</View>
{purposeComponent}
</View>
);

View file

@ -21,6 +21,7 @@ export default class FileAttachmentList extends Component {
fetchCache: PropTypes.object.isRequired,
files: PropTypes.array.isRequired,
hideOptionsContext: PropTypes.func.isRequired,
navigator: PropTypes.object,
onLongPress: PropTypes.func,
onPress: PropTypes.func,
post: PropTypes.object.isRequired,
@ -43,14 +44,33 @@ export default class FileAttachmentList extends Component {
}
}
goToImagePreview = (post, fileId) => {
this.props.navigator.showModal({
screen: 'ImagePreview',
title: '',
animationType: 'none',
passProps: {
fileId,
post
},
navigatorStyle: {
navBarHidden: true,
statusBarHidden: false,
statusBarHideWithNavBar: false,
screenBackgroundColor: 'transparent',
modalPresentationStyle: 'overCurrentContext'
}
});
};
handleInfoPress = () => {
this.props.hideOptionsContext();
this.props.onPress();
}
};
handlePreviewPress = (file) => {
this.props.hideOptionsContext();
preventDoubleTap(this.props.actions.goToImagePreviewModal, this, this.props.post, file.id);
preventDoubleTap(this.goToImagePreview, this, this.props.post, file.id);
};
render() {

View file

@ -8,7 +8,6 @@ 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 'app/selectors/preferences';
import {goToImagePreviewModal} from 'app/actions/navigation';
import FileAttachmentList from './file_attachment_list';
@ -29,7 +28,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
addFileToFetchCache,
goToImagePreviewModal,
loadFilesForPostIfNecessary
}, dispatch)
};

View file

@ -17,7 +17,7 @@ import FormattedText from 'app/components/formatted_text';
import {RequestStatus} from 'mattermost-redux/constants';
const INITIAL_TOP = Platform.OS === 'ios' ? -80 : -60;
const INITIAL_TOP = -46;
const OFFLINE = 'offline';
const CONNECTING = 'connecting';
const CONNECTED = 'connected';
@ -197,18 +197,18 @@ export default class OfflineIndicator extends PureComponent {
const styles = StyleSheet.create({
container: {
flex: 1,
zIndex: 10,
position: 'absolute',
elevation: 2
height: 38,
width: Dimensions.get('window').width,
zIndex: 9,
position: 'relative',
elevation: 1
},
wrapper: {
alignItems: 'center',
flex: 1,
height: 39,
flexDirection: 'row',
height: 38,
paddingHorizontal: 12,
width: Dimensions.get('window').width,
backgroundColor: 'red'
},
message: {

View file

@ -10,7 +10,6 @@ import {getCurrentUserId, getCurrentUserRoles, getUser} from 'mattermost-redux/s
import {isPostFlagged} from 'mattermost-redux/utils/post_utils';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import {goToUserProfile, openEditPostModal, requestCloseModal, showOptionsModal} from 'app/actions/navigation';
import {getTheme} from 'app/selectors/preferences';
import Post from './post';
@ -46,11 +45,7 @@ function mapDispatchToProps(dispatch) {
createPost,
deletePost,
flagPost,
goToUserProfile,
openEditPostModal,
removePost,
requestCloseModal,
showOptionsModal,
unflagPost
}, dispatch)
};

View file

@ -14,6 +14,7 @@ import {
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import Icon from 'react-native-vector-icons/Ionicons';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FileAttachmentList from 'app/components/file_attachment_list';
import FormattedText from 'app/components/formatted_text';
@ -23,6 +24,7 @@ import Markdown from 'app/components/markdown';
import OptionsContext from 'app/components/options_context';
import ProfilePicture from 'app/components/profile_picture';
import ReplyIcon from 'app/components/reply_icon';
import {NavigationTypes} from 'app/constants';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
@ -30,6 +32,7 @@ import webhookIcon from 'assets/images/icons/webhook.jpg';
import {Posts} from 'mattermost-redux/constants';
import DelayedAction from 'mattermost-redux/utils/delayed_action';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {canDeletePost, canEditPost, isSystemMessage} from 'mattermost-redux/utils/post_utils';
import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
@ -52,6 +55,7 @@ class Post extends PureComponent {
commentedOnDisplayName: PropTypes.string,
commentedOnPost: PropTypes.object,
license: PropTypes.object.isRequired,
navigator: PropTypes.object,
roles: PropTypes.string,
theme: PropTypes.object.isRequired,
onPress: PropTypes.func,
@ -59,12 +63,8 @@ class Post extends PureComponent {
createPost: PropTypes.func.isRequired,
deletePost: PropTypes.func.isRequired,
flagPost: PropTypes.func.isRequired,
goToUserProfile: PropTypes.func.isRequired,
openEditPostModal: PropTypes.func.isRequired,
removePost: PropTypes.func.isRequired,
unflagPost: PropTypes.func.isRequired,
requestCloseModal: PropTypes.func.isRequired,
showOptionsModal: PropTypes.func.isRequired
unflagPost: PropTypes.func.isRequired
}).isRequired
};
@ -91,6 +91,25 @@ class Post extends PureComponent {
this.editDisableAction.cancel();
}
goToUserProfile = (userId) => {
const {intl, navigator, theme} = this.props;
navigator.push({
screen: 'UserProfile',
title: intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}),
animated: true,
backButtonTitle: '',
passProps: {
userId
},
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
}
});
};
handleEditDisable = () => {
this.setState({canEdit: false});
};
@ -117,8 +136,25 @@ class Post extends PureComponent {
};
handlePostEdit = () => {
const {actions, post} = this.props;
actions.openEditPostModal(post);
const {intl, navigator, post, theme} = this.props;
MaterialIcon.getImageSource('close', 20, theme.sidebarHeaderTextColor).
then((source) => {
navigator.showModal({
screen: 'EditPost',
title: intl.formatMessage({id: 'mobile.edit_post.title', defaultMessage: 'Editing Message'}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
},
passProps: {
post,
closeButton: source
}
});
});
};
handleFailedPostPress = () => {
@ -131,7 +167,7 @@ class Post extends PureComponent {
action: () => {
const {failed, id, ...post} = this.props.post; // eslint-disable-line
this.props.actions.requestCloseModal();
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
this.props.actions.createPost(post);
},
text: {
@ -140,7 +176,7 @@ class Post extends PureComponent {
}
}, {
action: () => {
this.props.actions.requestCloseModal();
EventEmitter.emit(NavigationTypes.NAVIGATION_CLOSE_MODAL);
this.onRemovePost(this.props.post);
},
text: {
@ -153,7 +189,22 @@ class Post extends PureComponent {
}]
};
this.props.actions.showOptionsModal(options);
this.props.navigator.showModal({
screen: 'OptionsModal',
title: '',
animationType: 'none',
passProps: {
items: options.items,
title: options.title
},
navigatorStyle: {
navBarHidden: true,
statusBarHidden: false,
statusBarHideWithNavBar: false,
screenBackgroundColor: 'transparent',
modalPresentationStyle: 'overCurrentContext'
}
});
};
handlePress = () => {
@ -242,7 +293,7 @@ class Post extends PureComponent {
};
renderFileAttachments() {
const {post} = this.props;
const {navigator, post} = this.props;
const fileIds = post.file_ids || [];
let attachments;
@ -254,6 +305,7 @@ class Post extends PureComponent {
onPress={this.handlePress}
post={post}
toggleSelected={this.toggleSelected}
navigator={navigator}
/>
);
}
@ -389,7 +441,7 @@ class Post extends PureComponent {
};
viewUserProfile = () => {
preventDoubleTap(this.props.actions.goToUserProfile, null, this.props.user.id);
preventDoubleTap(this.goToUserProfile, null, this.props.user.id);
};
toggleSelected = (selected) => {

View file

@ -26,6 +26,7 @@ export default class PostList extends Component {
posts: PropTypes.array.isRequired,
theme: PropTypes.object.isRequired,
loadMore: PropTypes.func,
navigator: PropTypes.object,
isLoadingMore: PropTypes.bool,
showLoadMore: PropTypes.bool,
onPostPress: PropTypes.func,
@ -142,6 +143,7 @@ export default class PostList extends Component {
isLastReply={post.isLastReply}
commentedOnPost={post.commentedOnPost}
onPress={this.props.onPostPress}
navigator={this.props.navigator}
/>
);
};

View file

@ -6,7 +6,6 @@ import {connect} from 'react-redux';
import {createPost} from 'mattermost-redux/actions/posts';
import {userTyping} from 'mattermost-redux/actions/websocket';
import {showOptionsModal, requestCloseModal} from 'app/actions/navigation';
import {handleClearFiles, handleRemoveLastFile, handleUploadFiles} from 'app/actions/views/file_upload';
import {getTheme} from 'app/selectors/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
@ -29,11 +28,9 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
createPost,
closeModal: requestCloseModal,
handleClearFiles,
handleRemoveLastFile,
handleUploadFiles,
showOptionsModal,
userTyping
}, dispatch)
};

View file

@ -30,12 +30,10 @@ const MAX_MESSAGE_LENGTH = 4000;
class PostTextbox extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
closeModal: PropTypes.func.isRequired,
createPost: PropTypes.func.isRequired,
handleClearFiles: PropTypes.func.isRequired,
handleRemoveLastFile: PropTypes.func.isRequired,
handleUploadFiles: PropTypes.func.isRequired,
showOptionsModal: PropTypes.func.isRequired,
userTyping: PropTypes.func.isRequired
}).isRequired,
channelId: PropTypes.string.isRequired,
@ -43,6 +41,7 @@ class PostTextbox extends PureComponent {
currentUserId: PropTypes.string.isRequired,
files: PropTypes.array,
intl: intlShape.isRequired,
navigator: PropTypes.object,
onChangeText: PropTypes.func.isRequired,
rootId: PropTypes.string,
theme: PropTypes.object.isRequired,
@ -207,7 +206,9 @@ class PostTextbox extends PureComponent {
};
attachFileFromCamera = () => {
this.props.actions.closeModal();
this.props.navigator.dismissModal({
animationType: 'none'
});
const options = {
quality: 0.7,
@ -228,7 +229,9 @@ class PostTextbox extends PureComponent {
};
attachFileFromLibrary = () => {
this.props.actions.closeModal();
this.props.navigator.dismissModal({
animationType: 'none'
});
const options = {
quality: 0.7,
@ -268,7 +271,21 @@ class PostTextbox extends PureComponent {
}]
};
this.props.actions.showOptionsModal(options);
this.props.navigator.showModal({
screen: 'OptionsModal',
title: '',
animationType: 'none',
passProps: {
items: options.items
},
navigatorStyle: {
navBarHidden: true,
statusBarHidden: false,
statusBarHideWithNavBar: false,
screenBackgroundColor: 'transparent',
modalPresentationStyle: 'overCurrentContext'
}
});
};
renderTyping = () => {

View file

@ -1,23 +1,17 @@
// 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 Config from 'assets/config.json';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {closeDrawers, closeModal, goBack, unrenderDrawer} from 'app/actions/navigation';
import {goToNotification, loadConfigAndLicense, queueNotification} from 'app/actions/views/root';
import {setAppState, setDeviceToken} from 'mattermost-redux/actions/general';
import {logout} from 'mattermost-redux/actions/users';
import Config from 'assets/config.json';
import Root from './root';
function mapStateToProps(state, ownProps) {
const users = state.entities.users;
const {currentUserId} = users;
const {currentTeamId} = state.entities.teams;
const {currentChannelId} = state.entities.channels;
let locale = Config.DefaultLocale;
if (currentUserId && users.profiles[currentUserId]) {
@ -26,28 +20,9 @@ function mapStateToProps(state, ownProps) {
return {
...ownProps,
currentTeamId,
currentChannelId,
locale,
navigation: state.navigation
currentChannelId: getCurrentChannelId(state),
locale
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
closeDrawers,
closeModal,
goBack,
loadConfigAndLicense,
logout,
goToNotification,
queueNotification,
setAppState,
setDeviceToken,
unrenderDrawer
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Root);
export default connect(mapStateToProps)(Root);

View file

@ -1,128 +1,47 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component, PropTypes} from 'react';
import {
Alert,
AppState,
BackAndroid,
InteractionManager,
Platform,
StatusBar,
View
} from 'react-native';
import React, {PropTypes, PureComponent} from 'react';
import {IntlProvider} from 'react-intl';
import DeviceInfo from 'react-native-device-info';
import semver from 'semver';
import PushNotification from 'app/components/push_notification';
import {getTranslations} from 'app/i18n';
import Config from 'assets/config';
import {Client4} from 'mattermost-redux/client';
import {General} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
export default class Root extends Component {
import {ViewTypes} from 'app/constants';
import {getTranslations} from 'app/i18n';
export default class Root extends PureComponent {
static propTypes = {
children: PropTypes.node,
currentTeamId: PropTypes.string,
navigator: PropTypes.object,
currentChannelId: PropTypes.string,
locale: PropTypes.string.isRequired,
navigation: PropTypes.object.isRequired,
actions: PropTypes.shape({
closeDrawers: PropTypes.func.isRequired,
closeModal: PropTypes.func.isRequired,
goBack: PropTypes.func.isRequired,
loadConfigAndLicense: PropTypes.func.isRequired,
logout: PropTypes.func.isRequired,
setAppState: PropTypes.func.isRequired,
unrenderDrawer: PropTypes.func.isRequired
}).isRequired
locale: PropTypes.string.isRequired
};
componentDidMount() {
this.props.actions.setAppState(AppState.currentState === 'active');
AppState.addEventListener('change', this.handleAppStateChange);
EventEmitter.on(General.CONFIG_CHANGED, this.handleConfigChanged);
Client4.setUserAgent(DeviceInfo.getUserAgent());
if (Platform.OS === 'android') {
BackAndroid.addEventListener('hardwareBackPress', this.handleAndroidBack);
}
componentWillMount() {
EventEmitter.on(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification);
}
componentWillUnmount() {
AppState.removeEventListener('change', this.handleAppStateChange);
EventEmitter.off(General.CONFIG_CHANGED, this.handleConfigChanged);
if (Platform.OS === 'android') {
BackAndroid.removeEventListener('hardwareBackPress', this.handleAndroidBack);
}
EventEmitter.off(ViewTypes.NOTIFICATION_IN_APP, this.handleInAppNotification);
}
handleAppStateChange = (appState) => {
this.props.actions.setAppState(appState === 'active');
handleInAppNotification = (notification) => {
const {data} = notification;
const {currentChannelId, navigator} = this.props;
if (appState === 'inactive') {
// TODO: See if we still need this
if (data.channel_id !== currentChannelId) {
navigator.showInAppNotification({
screen: 'Notification',
position: 'top',
autoDismissTimerSec: 15,
dismissWithSwipe: true,
passProps: {
notification
}
});
}
};
handleAndroidBack = () => {
const {index, isModal, leftDrawerOpen, modal} = this.props.navigation;
const {closeDrawers, closeModal, goBack} = this.props.actions;
if (isModal) {
if (modal.index > 0) {
goBack();
return true;
}
closeModal();
return true;
}
if (leftDrawerOpen) {
closeDrawers();
return true;
} else if (index > 0) {
goBack();
return true;
}
return false;
};
handleConfigChanged = (serverVersion) => {
const {loadConfigAndLicense} = this.props.actions;
const version = serverVersion.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g)[0];
if (serverVersion) {
if (semver.valid(version) && semver.lt(version, Config.MinServerVersion)) {
Alert.alert(
'Server upgrade required',
'A server upgrade is required to use the Mattermost app. Please ask your System Administrator for details.',
[{
text: 'OK',
onPress: this.handleVersionUpgrade
}]
);
} else {
loadConfigAndLicense(serverVersion);
}
}
};
handleVersionUpgrade = async () => {
const {closeDrawers, logout, unrenderDrawer} = this.props.actions;
Client4.serverVersion = '';
closeDrawers();
unrenderDrawer();
InteractionManager.runAfterInteractions(logout);
};
render() {
const locale = this.props.locale;
@ -131,11 +50,7 @@ export default class Root extends Component {
locale={locale}
messages={getTranslations(locale)}
>
<View style={{flex: 1}}>
<StatusBar barStyle='light-content'/>
{this.props.children}
<PushNotification/>
</View>
{this.props.children}
</IntlProvider>
);
}

View file

@ -2,11 +2,9 @@
// See License.txt for license information.
import NavigationTypes from './navigation';
import StorageTypes from './storage';
import ViewTypes from './view';
export {
NavigationTypes,
StorageTypes,
ViewTypes
};

View file

@ -4,21 +4,8 @@
import keyMirror from 'mattermost-redux/utils/key_mirror';
const NavigationTypes = keyMirror({
NAVIGATION_PUSH: null,
NAVIGATION_POP: null,
NAVIGATION_POP_TO_INDEX: null,
NAVIGATION_OPEN_LEFT_DRAWER: null,
NAVIGATION_RENDER_LEFT_DRAWER: null,
NAVIGATION_OPEN_RIGHT_DRAWER: null,
NAVIGATION_CLOSE_DRAWERS: null,
NAVIGATION_JUMP: null,
NAVIGATION_JUMP_TO_INDEX: null,
NAVIGATION_REPLACE: null,
NAVIGATION_REPLACE_AT_ROOT: null,
NAVIGATION_RESET: null,
NAVIGATION_MODAL: null,
NAVIGATION_CLOSE_MODAL: null,
NAVIGATION_REQUEST_CLOSE_MODAL: null
NAVIGATION_CLOSE_MODAL: null
});
export default NavigationTypes;

View file

@ -1,15 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import keyMirror from 'mattermost-redux/utils/key_mirror';
const StorageTypes = keyMirror({
SAVE_TO_STORAGE: null,
SAVE_TO_STORAGE_ERROR: null,
LOAD_FROM_STORAGE: null,
LOAD_FROM_STORAGE_ERROR: null,
REMOVE_FROM_STORAGE: null,
REMOVE_FROM_STORAGE_ERROR: null
});
export default StorageTypes;

View file

@ -14,9 +14,8 @@ const ViewTypes = keyMirror({
POST_DRAFT_CHANGED: null,
COMMENT_DRAFT_CHANGED: null,
OPTIONS_MODAL_CHANGED: null,
NOTIFICATION_CHANGED: null,
NOTIFICATION_IN_APP: null,
CONNECTION_CHANGED: null,

View file

@ -3,8 +3,6 @@
import Config from 'assets/config.json';
import Routes from 'app/navigation/routes';
const state = {
entities: {
general: {
@ -252,23 +250,7 @@ const state = {
}
}
},
navigation: {
index: 0,
routes: [
Routes.Root
],
modal: {
index: 0,
requestClose: false,
routes: []
},
isModal: false,
leftDrawerOpen: false,
leftDrawerRoute: Routes.ChannelDrawer,
rightDrawerOpen: false,
rightDrawerRoute: null,
shouldRenderDrawer: false
},
navigation: '',
views: {
channel: {
drafts: {},
@ -284,12 +266,8 @@ const state = {
password: ''
},
notification: null,
optionsModal: {
title: '',
options: [],
visible: false
},
root: {
appInitializing: false,
hydrationComplete: false
},
selectServer: {

View file

@ -1,49 +1,169 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import 'harmony-reflect';
import React from 'react';
import 'babel-polyfill';
import Orientation from 'react-native-orientation';
import {Provider} from 'react-redux';
import {Navigation} from 'react-native-navigation';
import DeviceNotification from 'react-native-push-notification';
import {
Alert,
AppState,
InteractionManager,
Platform
} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import semver from 'semver';
import Root from 'app/components/root';
import Router from 'app/navigation/router';
import {setAppState, setDeviceToken} from 'mattermost-redux/actions/general';
import {logout} from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import {General} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {goToNotification, loadConfigAndLicense, queueNotification} from 'app/actions/views/root';
import {NavigationTypes, ViewTypes} from 'app/constants';
import initialState from 'app/initial_state';
import {registerScreens} from 'app/screens';
import configureStore from 'app/store';
import initialState from './initial_state';
import Config from 'assets/config';
const store = configureStore(initialState);
registerScreens(store, Provider);
export default class Mattermost extends React.Component {
state = {
hydrated: false
export default class Mattermost {
constructor() {
this.isConfigured = false;
Orientation.lockToPortrait();
this.unsubscribeFromStore = store.subscribe(this.listenForHydration);
AppState.addEventListener('change', this.handleAppStateChange);
EventEmitter.on(General.CONFIG_CHANGED, this.handleConfigChanged);
EventEmitter.on(NavigationTypes.NAVIGATION_RESET, this.handleReset);
this.handleAppStateChange(AppState.currentState);
Client4.setUserAgent(DeviceInfo.getUserAgent());
}
handleAppStateChange = (appState) => {
const {dispatch, getState} = store;
setAppState(appState === 'active')(dispatch, getState);
};
componentWillMount() {
this.unsubscribeFromStore = store.subscribe(this.listenForHydration);
}
handleConfigChanged = (serverVersion) => {
const {dispatch, getState} = store;
const version = serverVersion.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g)[0];
if (serverVersion) {
if (semver.valid(version) && semver.lt(version, Config.MinServerVersion)) {
Alert.alert(
'Server upgrade required',
'A server upgrade is required to use the Mattermost app. Please ask your System Administrator for details.',
[{
text: 'OK',
onPress: this.handleVersionUpgrade
}]
);
} else {
loadConfigAndLicense(serverVersion)(dispatch, getState);
}
}
};
handleReset = () => {
Client4.serverVersion = '';
this.startApp('fade');
};
handleVersionUpgrade = async () => {
const {dispatch, getState} = store;
// const {closeDrawers, logout, unrenderDrawer} = this.props.actions;
Client4.serverVersion = '';
// closeDrawers();
// unrenderDrawer();
if (getState().entities.general.credentials.token) {
InteractionManager.runAfterInteractions(() => {
logout()(dispatch, getState);
});
}
};
// We need to wait for hydration to occur before load the router.
listenForHydration = () => {
const state = store.getState();
if (state.views.root.hydrationComplete) {
this.unsubscribeFromStore();
this.setState({
hydrated: true
});
this.startApp();
}
};
render() {
if (!this.state.hydrated) {
return null;
configurePushNotifications = () => {
DeviceNotification.configure({
onRegister: this.onRegisterDevice,
onNotification: this.onPushNotification,
senderID: Config.GooglePlaySenderId,
popInitialNotification: true,
requestPermissions: true
});
};
onRegisterDevice = (data) => {
const prefix = Platform.OS === 'ios' ? General.PUSH_NOTIFY_APPLE_REACT_NATIVE : General.PUSH_NOTIFY_ANDROID_REACT_NATIVE;
const {dispatch, getState} = store;
setDeviceToken(`${prefix}:${data.token}`)(dispatch, getState);
this.isConfigured = true;
};
onPushNotification = (deviceNotification) => {
const {foreground, userInteraction, data, message} = deviceNotification;
let notification;
if (Platform.OS === 'android') {
notification = {
data: {
channel_id: deviceNotification.channel_id,
team_id: deviceNotification.team_id
},
message
};
} else {
notification = {
data,
message
};
}
return (
<Provider store={store}>
<Root>
<Router/>
</Root>
</Provider>
);
}
if (foreground) {
EventEmitter.emit(ViewTypes.NOTIFICATION_IN_APP, notification);
} else if (userInteraction) {
const {dispatch, getState} = store;
const state = getState();
if (!state.views.root.appInitializing) {
// go to notification if the app is initialized
goToNotification(notification)(dispatch, getState);
} else if (state.entities.general.credentials.token) {
// queue notification if app is not initialized but we are logged in
queueNotification(notification)(dispatch, getState);
}
}
};
startApp = (animationType = 'none') => {
this.configurePushNotifications();
Navigation.startSingleScreenApp({
screen: {
screen: 'Root',
navigatorStyle: {
navBarHidden: true,
statusBarHidden: false,
statusBarHideWithNavBar: false
}
},
animationType
});
};
}

View file

@ -1,172 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {
Animated,
InteractionManager
} from 'react-native';
const {View: AnimatedView} = Animated;
const ANIMATION_TYPES = {
SlideFromBottom: 'slideFromBottom',
Fade: 'fade'
};
export default class NavigationModal extends PureComponent {
static propTypes = {
animationType: PropTypes.oneOf([
ANIMATION_TYPES.SlideFromBottom,
ANIMATION_TYPES.Fade
]),
children: PropTypes.node,
deviceHeight: PropTypes.number.isRequired,
deviceWidth: PropTypes.number.isRequired,
duration: PropTypes.number,
show: PropTypes.bool
}
static defaultProps = {
animationType: ANIMATION_TYPES.SlideFromBottom,
duration: 400,
show: false
}
constructor(props) {
super(props);
const top = props.show ? 0 : props.deviceHeight;
this.state = {
top: new Animated.Value(top),
opacity: new Animated.Value(100)
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.show) {
let currentRouteKeys;
const nextRouteKeys = nextProps.children.props.navigationState.routes.map((r) => r.key).join('');
if (this.state.children) {
currentRouteKeys = this.state.children.props.navigationState.routes.map((r) => r.key).join('');
}
const routesDidChange = currentRouteKeys !== nextRouteKeys;
// In order for the scene to be shown throughout the
// animated slide down we have to hang on to it by
// storing it in state
if (routesDidChange) {
this.setState({
children: nextProps.children
});
}
}
if (nextProps.show !== this.props.show) {
const animationType = nextProps.show ? nextProps.animationType : this.props.animationType;
if (animationType === ANIMATION_TYPES.SlideFromBottom) {
InteractionManager.runAfterInteractions(() => {
this.slideFromBottomAnimationRunner(nextProps);
});
} else if (animationType === ANIMATION_TYPES.Fade) {
InteractionManager.runAfterInteractions(() => {
this.fadeAnimationRunner(nextProps);
});
}
}
}
fadeAnimationRunner = (nextProps) => {
if (nextProps.show) {
const setOpacityToZero = Animated.timing(this.state.opacity, {
toValue: 0,
duration: 0
});
const setTopToZero = Animated.timing(this.state.top, {
toValue: 0,
duration: 0
});
const setOpacityToFull = Animated.timing(this.state.opacity, {
toValue: 1,
duration: nextProps.duration
});
Animated.sequence([
setOpacityToZero,
setTopToZero,
setOpacityToFull
]).start(() => {
// Once the scene has finished sliding down we can release the child scene
// which will unmount the scene correctly.
if (!this.props.show && !nextProps.show) {
this.setState({
children: null
});
}
});
} else {
const setOpacityToZero = Animated.timing(this.state.opacity, {
toValue: 0,
duration: nextProps.duration
});
const setTopToDeviceHeight = Animated.timing(this.state.top, {
toValue: this.props.deviceHeight,
duration: 0
});
const setOpacityToFull = Animated.timing(this.state.opacity, {
toValue: 1,
duration: 0
});
Animated.sequence([
setOpacityToZero,
setTopToDeviceHeight,
setOpacityToFull
]).start(() => {
// Once the scene has finished sliding down we can release the child scene
// which will unmount the scene correctly.
if (!this.props.show && !nextProps.show) {
this.setState({
children: null
});
}
});
}
}
slideFromBottomAnimationRunner = (nextProps) => {
const animateValue = nextProps.show ? 0 : this.props.deviceHeight;
Animated.timing(this.state.top, {
toValue: animateValue,
duration: nextProps.duration
}).start(() => {
// Once the scene has finished sliding down we can release the child scene
// which will unmount the scene correctly.
if (!this.props.show && !nextProps.show) {
this.setState({
children: null
});
}
});
}
render() {
return (
<AnimatedView
onLayout={this.onLayout}
style={{
position: 'absolute',
width: this.props.deviceWidth,
height: this.props.deviceHeight,
top: this.state.top,
left: 0,
backgroundColor: '#0000',
opacity: this.state.opacity
}}
>
{this.state.children}
</AnimatedView>
);
}
}

View file

@ -1,360 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component, PropTypes} from 'react';
import {
Dimensions,
Easing,
InteractionManager,
Keyboard,
NavigationExperimental,
View
} from 'react-native';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {closeDrawers, goBack, openChannelDrawer} from 'app/actions/navigation';
import Drawer from 'app/components/drawer';
import FormattedText from 'app/components/formatted_text';
import {RouteTransitions} from 'app/navigation/routes';
import {getTheme} from 'app/selectors/preferences';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import NavigationModal from './navigation_modal';
const navigationPanResponder = NavigationExperimental.Card.CardStackPanResponder;
const {width: deviceWidth, height: deviceHeight} = Dimensions.get('window');
class Router extends Component {
static propTypes = {
navigation: PropTypes.object,
theme: PropTypes.object,
actions: PropTypes.shape({
closeDrawers: PropTypes.func.isRequired,
goBack: PropTypes.func.isRequired,
openChannelDrawer: PropTypes.func.isRequired
}).isRequired
};
headerEventSubscriptions = {};
state = {
deviceHeight,
deviceWidth
};
emitHeaderEvent = (key) => (event) => {
const subscription = this.headerEventSubscriptions[`${key}-${event}`];
if (subscription) {
subscription();
}
};
subscribeToHeaderEvent = (key) => (event, callback) => {
this.headerEventSubscriptions[`${key}-${event}`] = callback;
};
unsubscribeFromHeaderEvent = (key) => (event) => {
if (this.headerEventSubscriptions[event]) {
Reflect.deleteProperty(this.headerEventSubscriptions, `${key}-${event}`);
}
};
wrapHeaderComponent = (fx, emitter) => (props) => {
if (fx && props.scene.isActive) {
return fx(props, emitter, this.props.theme);
}
return null;
};
extractNavigationProps = (route) => {
if (!route) {
return {};
}
let component = route.component;
if (!component) {
component = {};
}
return Object.assign({}, route.navigationProps, component.navigationProps);
};
renderTransition = (transitionProps) => {
const navigationProps = this.extractNavigationProps(transitionProps.scene.route);
let navBar = null;
if (!navigationProps.hideNavBar) {
const emitter = this.emitHeaderEvent(transitionProps.scene.route.key);
const renderLeftComponent = transitionProps.navigationState.index > 0 ? this.wrapHeaderComponent(navigationProps.renderBackButton, emitter) : this.wrapHeaderComponent(navigationProps.renderLeftComponent, emitter);
const renderTitleComponent = navigationProps.renderTitleComponent ? this.wrapHeaderComponent(navigationProps.renderTitleComponent, emitter) : this.renderTitle;
const renderRightComponent = this.wrapHeaderComponent(navigationProps.renderRightComponent, emitter);
navBar = (
<NavigationExperimental.Header
{...transitionProps}
onNavigateBack={this.props.actions.goBack}
renderLeftComponent={renderLeftComponent}
renderTitleComponent={renderTitleComponent}
renderRightComponent={renderRightComponent}
style={[{backgroundColor: this.props.theme.sidebarHeaderBg}, navigationProps.headerStyle]}
/>
);
}
const renderedScenes = transitionProps.scenes.map((scene) => {
const cardProps = {
...transitionProps,
scene
};
let style;
if (scene.route.transition === RouteTransitions.Horizontal) {
style = NavigationExperimental.Card.PagerStyleInterpolator.forHorizontal({
...cardProps
});
} else {
// have to override shadow props for transparent modals
style = {
backgroundColor: 'transparent',
shadowColor: null,
shadowOffset: null,
shadowOpacity: null,
shadowRadius: null
};
}
let panHandlers = null;
if (navigationProps.allowSceneSwipe) {
panHandlers = navigationPanResponder.forHorizontal({
...cardProps,
gestureResponseDistance: (this.state.deviceWidth / 4), // sets the distance from the edge for swiping
onNavigateBack: this.props.actions.goBack
});
}
return (
<NavigationExperimental.Card
{...cardProps}
onNavigateBack={this.props.actions.goBack}
panHandlers={panHandlers}
style={style}
renderScene={this.renderScene}
key={scene.key}
/>
);
});
return (
<View style={{flex: 1, flexDirection: 'column-reverse'}}>
<View style={{flex: 1}}>
{renderedScenes}
</View>
{navBar}
</View>
);
};
renderTitle = ({scene}) => {
const navigationProps = this.extractNavigationProps(scene.route);
const title = navigationProps.title;
if (!title) {
return null;
}
return (
<NavigationExperimental.Header.Title>
<FormattedText
id={title.id}
defaultMessage={title.defaultMessage}
/>
</NavigationExperimental.Header.Title>
);
};
renderScene = ({scene}) => {
return this.renderRoute(scene.route);
};
renderRoute = (route) => {
const SceneComponent = route.component;
const emitterSubscriber = this.subscribeToHeaderEvent(route.key);
const emitterUnsubscriber = this.unsubscribeFromHeaderEvent(route.key);
return (
<SceneComponent
subscribeToHeaderEvent={emitterSubscriber}
unsubscribeFromHeaderEvent={emitterUnsubscriber}
{...route.props}
/>
);
};
configureTransition = () => {
return {
duration: 300,
easing: Easing.inOut(Easing.ease)
};
};
onLayout = (event) => {
if (event.nativeEvent.layout.width !== this.state.deviceWidth) {
this.setState({
deviceHeight: event.nativeEvent.layout.height,
deviceWidth: event.nativeEvent.layout.width
});
}
};
handleDrawerTween = (ratio) => {
const opacity = (ratio / 2);
EventEmitter.emit('drawer_opacity', opacity);
return {
mainOverlay: {
backgroundColor: this.props.theme.centerChannelBg,
opacity
},
drawerOverlay: {
backgroundColor: ratio ? '#000' : '#FFF',
opacity: ratio ? (1 - ratio) / 2 : 1
}
};
};
handleLeftDrawerClose = () => {
setTimeout(() => {
InteractionManager.clearInteractionHandle(this.closeLeftHandle);
});
};
handleLeftDrawerCloseStart = () => {
this.closeLeftHandle = InteractionManager.createInteractionHandle();
};
handleLeftDrawerOpen = () => {
Keyboard.dismiss();
setTimeout(() => {
InteractionManager.clearInteractionHandle(this.openLeftHandle);
});
};
handleLeftDrawerOpenStart = () => {
this.props.actions.openChannelDrawer();
this.openLeftHandle = InteractionManager.createInteractionHandle();
};
render = () => {
const {
index,
isModal: modalVisible,
leftDrawerOpen,
leftDrawerRoute,
modal,
shouldRenderDrawer,
routes
} = this.props.navigation;
const navigationProps = this.extractNavigationProps(routes[index]);
const modalNavigationProps = this.extractNavigationProps(modal.routes[0]);
let leftDrawerContent;
if (leftDrawerRoute && shouldRenderDrawer) {
leftDrawerContent = this.renderRoute(leftDrawerRoute);
}
return (
<View
style={{flex: 1}}
onLayout={this.onLayout}
>
<Drawer
open={leftDrawerOpen}
onOpenStart={this.handleLeftDrawerOpenStart}
onOpen={this.handleLeftDrawerOpen}
onCloseStart={this.handleLeftDrawerCloseStart}
onClose={this.handleLeftDrawerClose}
type='static'
disabled={modalVisible}
content={leftDrawerContent}
tapToClose={true}
openDrawerOffset={40}
onRequestClose={this.props.actions.closeDrawers}
panOpenMask={0.2}
panCloseMask={40}
panThreshold={0.25}
acceptPan={navigationProps.allowMenuSwipe}
negotiatePan={true}
useInteractionManager={false}
tweenHandler={this.handleDrawerTween}
elevation={-5}
styles={{
main: {shadowColor: '#000000', shadowOpacity: 0.4, shadowRadius: 12, shadowOffset: {width: -4, height: 0}}
}}
>
<Drawer
open={false}
type='displace'
side='right'
disabled={true}
content={null}
tapToClose={true}
openDrawerOffset={50}
onRequestClose={this.props.actions.closeDrawers}
panOpenMask={0}
panCloseMask={50}
panThreshold={0}
acceptPan={false}
negotiatePan={false}
useInteractionManager={false}
tweenHandler={this.handleDrawerTween}
>
<NavigationExperimental.Transitioner
style={{flex: 1}}
navigationState={this.props.navigation}
render={this.renderTransition}
configureTransition={this.configureTransition}
/>
</Drawer>
</Drawer>
<NavigationModal
animationType={modalNavigationProps.modalAnimationType}
deviceHeight={this.state.deviceHeight}
deviceWidth={this.state.deviceWidth}
duration={modalNavigationProps.duration}
show={modalVisible}
>
<NavigationExperimental.Transitioner
style={{flex: 1}}
navigationState={modal}
render={this.renderTransition}
configureTransition={this.configureTransition}
/>
</NavigationModal>
</View>
);
};
}
function mapStateToProps(state) {
return {
navigation: state.navigation,
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
closeDrawers,
goBack,
openChannelDrawer
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Router);

View file

@ -1,225 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {
About,
AccountNotifications,
AccountSettings,
ChannelView,
ChannelDrawer,
ChannelInfo,
ChannelMembers,
ChannelAddMembers,
CreateChannel,
EditPost,
ImagePreview,
LoadTeam,
Login,
LoginOptions,
Mfa,
MoreChannels,
MoreDirectMessages,
OptionsModal,
Settings,
Root,
Saml,
Search,
SelectServer,
SelectTeam,
Thread,
UserProfile
} from 'app/scenes';
import keyMirror from 'mattermost-redux/utils/key_mirror';
export const RouteTransitions = keyMirror({
Horizontal: null
});
export const Routes = {
About: {
key: 'About',
transition: RouteTransitions.Horizontal,
component: About,
navigationProps: {
title: {id: 'about.titles', defaultMessage: 'About Mattermost'}
}
},
AccountNotifications: {
key: 'About',
transition: RouteTransitions.Horizontal,
component: AccountNotifications,
navigationProps: {
title: {id: 'user.settings.modal.notifications', defaultMessage: 'Notifications'}
}
},
AccountSettings: {
key: 'AccountSettings',
transition: RouteTransitions.Horizontal,
component: AccountSettings,
navigationProps: {
title: {id: 'user.settings.modal.title', defaultMessage: 'Account Settings'}
}
},
ChannelInfo: {
key: 'ChannelInfo',
transition: RouteTransitions.Horizontal,
component: ChannelInfo,
navigationProps: {
title: {id: 'mobile.routes.channelInfo', defaultMessage: 'Info'}
}
},
ChannelDrawer: {
key: 'ChannelDrawer',
component: ChannelDrawer
},
ChannelMembers: {
key: 'ChannelMembers',
transition: RouteTransitions.Horizontal,
component: ChannelMembers,
navigationProps: {
title: {id: 'channel_header.manageMembers', defaultMessage: 'Manage Members'}
}
},
ChannelAddMembers: {
key: 'ChannelAddMembers',
transition: RouteTransitions.Horizontal,
component: ChannelAddMembers,
navigationProps: {
title: {id: 'channel_header.addMembers', defaultMessage: 'Add Members'}
}
},
ChannelView: {
key: 'ChannelView',
transition: RouteTransitions.Horizontal,
component: ChannelView
},
CreatePublicChannel: {
key: 'CreatePublicChannel',
component: CreateChannel,
transition: RouteTransitions.Horizontal,
navigationProps: {
title: {id: 'mobile.create_channel.public', defaultMessage: 'New Public Channel'}
}
},
CreatePrivateChannel: {
key: 'CreatePrivateChannel',
component: CreateChannel,
navigationProps: {
title: {id: 'mobile.create_channel.private', defaultMessage: 'New Private Channel'}
}
},
EditPost: {
key: 'EditPost',
component: EditPost,
navigationProps: {
title: {id: 'mobile.edit_post.title', defaultMessage: 'Editing Message'}
}
},
ImagePreview: {
key: 'ImagePreview',
component: ImagePreview
},
LoadTeam: {
key: 'LoadTeam',
component: LoadTeam
},
Login: {
key: 'Login',
transition: RouteTransitions.Horizontal,
component: Login,
navigationProps: {
title: {id: 'mobile.routes.login', defaultMessage: 'Login'}
}
},
LoginOptions: {
key: 'LoginOptions',
transition: RouteTransitions.Horizontal,
component: LoginOptions,
navigationProps: {
title: {id: 'mobile.routes.loginOptions', defaultMessage: 'Login Chooser'}
}
},
Mfa: {
key: 'Mfa',
transition: RouteTransitions.Horizontal,
component: Mfa,
navigationProps: {
title: {id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'}
}
},
MoreChannels: {
key: 'MoreChannels',
component: MoreChannels,
navigationProps: {
title: {id: 'more_channels.title', defaultMessage: 'More Channels'}
}
},
MoreDirectMessages: {
key: 'MoreDirectMessages',
component: MoreDirectMessages,
navigationProps: {
title: {id: 'more_direct_channels.title', defaultMessage: 'Direct Messages'}
}
},
OptionsModal: {
key: 'OptionsModal',
component: OptionsModal
},
Root: {
key: 'Root',
component: Root
},
Saml: {
key: 'Saml',
transition: RouteTransitions.Horizontal,
component: Saml,
navigationProps: {
title: {id: 'mobile.routes.saml', defaultMessage: 'Single SignOn'}
}
},
Search: {
key: 'Search',
transition: RouteTransitions.Horizontal,
component: Search
},
SelectServer: {
key: 'SelectServer',
component: SelectServer,
navigationProps: {
title: {id: 'mobile.routes.enterServerUrl', defaultMessage: 'Enter Server URL'},
duration: 0,
modalAnimationType: 'fade'
}
},
SelectTeam: {
key: 'SelectTeam',
component: SelectTeam,
transition: RouteTransitions.Horizontal,
navigationProps: {
title: {id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'}
}
},
Settings: {
key: 'Settings',
component: Settings,
navigationProps: {
title: {id: 'mobile.routes.settings', defaultMessage: 'Settings'}
}
},
Thread: {
key: 'Thread',
transition: RouteTransitions.Horizontal,
component: Thread
},
UserProfile: {
key: 'UserProfile',
transition: RouteTransitions.Horizontal,
component: UserProfile,
navigationProps: {
title: {id: 'mobile.routes.user_profile', defaultMessage: 'Profile'}
}
}
};
export default Routes;

View file

@ -1,229 +1,16 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {NavigationExperimental} from 'react-native';
import {UserTypes} from 'mattermost-redux/action_types';
import {NavigationTypes} from 'app/constants';
import {UserTypes} from 'mattermost-redux/action_types';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import Routes from 'app/navigation/routes';
const initialState = {
index: 0,
routes: [
Routes.Root
],
modal: {
index: 0,
requestClose: false,
routes: []
},
isModal: false,
leftDrawerOpen: false,
leftDrawerRoute: null,
rightDrawerOpen: false,
rightDrawerRoute: null,
shouldRenderDrawer: false
};
export default function(state = initialState, action) {
export default function(state = '', action) {
switch (action.type) {
case NavigationTypes.NAVIGATION_PUSH: {
if (state.isModal) {
const modalState = NavigationExperimental.StateUtils.push(state.modal, {props: action.props, ...action.route});
return {
...state,
modal: modalState
};
}
return NavigationExperimental.StateUtils.push(state, action.route);
}
case NavigationTypes.NAVIGATION_POP:
if (!state.isModal && (state.leftDrawerOpen || state.rightDrawerOpen)) {
return {
...state,
leftDrawerOpen: false,
rightDrawerOpen: false
};
}
if (state.isModal) {
const nextModalState = state.modal.routes.length > 1 ?
NavigationExperimental.StateUtils.pop(state.modal) :
{
index: 0,
routes: []
};
nextModalState.requestClose = false;
const isModal = nextModalState.routes.length > 0;
return {
...state,
modal: nextModalState,
isModal
};
}
return NavigationExperimental.StateUtils.pop(state);
case NavigationTypes.NAVIGATION_POP_TO_INDEX: {
let newState = {...state};
if (!newState.isModal && (newState.leftDrawerOpen || newState.rightDrawerOpen)) {
newState = {
...newState,
leftDrawerOpen: false,
rightDrawerOpen: false
};
}
if (newState.isModal) {
newState = {
...newState,
modal: {
index: 0,
routes: []
},
isModal: false
};
}
if (action.index === newState.index || action.index > newState.routes.length - 1) {
return newState;
}
const nextRoutes = newState.routes.slice(0, action.index + 1);
return {
...newState,
index: action.index,
routes: nextRoutes
};
}
case NavigationTypes.NAVIGATION_OPEN_LEFT_DRAWER:
return {
...state,
leftDrawerOpen: true,
leftDrawerRoute: action.route
};
case NavigationTypes.NAVIGATION_OPEN_RIGHT_DRAWER:
return {
...state,
rightDrawerOpen: true,
rightDrawerRoute: action.route
};
case NavigationTypes.NAVIGATION_CLOSE_DRAWERS:
return {
...state,
leftDrawerOpen: false,
rightDrawerOpen: false
};
case NavigationTypes.NAVIGATION_JUMP: {
if (state.isModal) {
const modalState = NavigationExperimental.StateUtils.jumpTo(state.modal, action.key);
return {
...state,
modal: modalState
};
}
return NavigationExperimental.StateUtils.jumpTo(state, action.key);
}
case NavigationTypes.NAVIGATION_JUMP_TO_INDEX: {
if (state.isModal) {
const modalState = NavigationExperimental.StateUtils.jumpToIndex(state.modal, action.index);
return {
...state,
modal: modalState
};
}
return NavigationExperimental.StateUtils.jumpToIndex(state, action.index);
}
case NavigationTypes.NAVIGATION_RESET:
return {
...state,
...NavigationExperimental.StateUtils.reset(state, action.routes),
modal: {
index: 0,
requestClose: false,
routes: []
},
isModal: false,
leftDrawerOpen: false,
rightDrawerOpen: false
};
case NavigationTypes.NAVIGATION_REPLACE: {
if (state.isModal) {
const modalState = NavigationExperimental.StateUtils.replaceAtIndex(state.modal, state.modal.index, action.index);
return {
...state,
modal: modalState
};
}
return NavigationExperimental.StateUtils.replaceAtIndex(state, state.index, action.route);
}
case NavigationTypes.NAVIGATION_REPLACE_AT_ROOT:
return NavigationExperimental.StateUtils.replaceAtIndex(state, state.index, action.route);
case NavigationTypes.NAVIGATION_MODAL: {
const modal = {
index: 0,
routes: [
{
...action.route
}
]
};
return {
...state,
modal,
isModal: true
};
}
case NavigationTypes.NAVIGATION_CLOSE_MODAL:
return {
...state,
modal: {
index: 0,
requestClose: false,
routes: []
},
isModal: false
};
case NavigationTypes.NAVIGATION_REQUEST_CLOSE_MODAL: {
const modalState = Object.assign({}, state.modal, {requestClose: true});
return {
...state,
modal: modalState
};
}
case NavigationTypes.NAVIGATION_RENDER_LEFT_DRAWER: {
return {
...state,
shouldRenderDrawer: action.data
};
}
case UserTypes.LOGOUT_SUCCESS:
return NavigationExperimental.StateUtils.reset(state, initialState.routes, initialState.index);
default:
return state;
EventEmitter.emit(NavigationTypes.NAVIGATION_RESET);
break;
}
return state;
}

View file

@ -2,7 +2,7 @@
// See License.txt for license information.
import {combineReducers} from 'redux';
import {ChannelTypes, FileTypes, UserTypes} from 'mattermost-redux/action_types';
import {ChannelTypes, FileTypes} from 'mattermost-redux/action_types';
import {ViewTypes} from 'app/constants';
@ -172,19 +172,7 @@ function loading(state = false, action) {
}
}
function appInitializing(state = true, action) {
switch (action.type) {
case ViewTypes.APPLICATION_INITIALIZED:
return false;
case UserTypes.RESET_LOGOUT_STATE:
return true;
default:
return state;
}
}
export default combineReducers({
appInitializing,
drafts,
loading
});

View file

@ -9,7 +9,6 @@ import fetchCache from './fetch_cache';
import i18n from './i18n';
import login from './login';
import notification from './notification';
import optionsModal from './options_modal';
import root from './root';
import selectServer from './select_server';
import team from './team';
@ -22,7 +21,6 @@ export default combineReducers({
i18n,
login,
notification,
optionsModal,
root,
selectServer,
team,

View file

@ -1,46 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {combineReducers} from 'redux';
import {UserTypes} from 'mattermost-redux/action_types';
import {ViewTypes} from 'app/constants';
function title(state = '', action) {
switch (action.type) {
case ViewTypes.OPTIONS_MODAL_CHANGED:
return action.data.title;
case UserTypes.LOGOUT_SUCCESS:
return '';
default:
return state;
}
}
function options(state = [], action) {
switch (action.type) {
case ViewTypes.OPTIONS_MODAL_CHANGED:
return action.data.options;
case UserTypes.LOGOUT_SUCCESS:
return [];
default:
return state;
}
}
function visible(state = false, action) {
switch (action.type) {
case ViewTypes.OPTIONS_MODAL_CHANGED:
return action.data.visible;
case UserTypes.LOGOUT_SUCCESS:
return false;
default:
return state;
}
}
export default combineReducers({
title,
options,
visible
});

View file

@ -2,7 +2,22 @@
// See License.txt for license information.
import {combineReducers} from 'redux';
import {General} from 'mattermost-redux/constants';
import {UserTypes} from 'mattermost-redux/action_types';
import {ViewTypes} from 'app/constants';
function appInitializing(state = true, action) {
switch (action.type) {
case ViewTypes.APPLICATION_INITIALIZED:
return false;
case UserTypes.RESET_LOGOUT_STATE:
return true;
default:
return state;
}
}
function hydrationComplete(state = false, action) {
switch (action.type) {
@ -14,5 +29,6 @@ function hydrationComplete(state = false, action) {
}
export default combineReducers({
appInitializing,
hydrationComplete
});

View file

@ -1,95 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {connect} from 'react-redux';
import {
TouchableOpacity,
View
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import {getTheme} from 'app/selectors/preferences';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
class AccountNotifcationsButton extends PureComponent {
static propTypes = {
emitter: PropTypes.func.isRequired,
theme: PropTypes.object
};
static defaultProps = {
theme: {}
};
constructor(props) {
super(props);
this.state = {
loading: false
};
}
componentWillMount() {
EventEmitter.on('saving_notify_props', this.onLoading);
}
componentWillUnmount() {
EventEmitter.off('saving_notify_props', this.onLoading);
}
onLoading = (loading) => {
this.setState({loading});
};
onPress = () => {
this.props.emitter('save_notify_props');
};
render() {
const {theme} = this.props;
const {loading} = this.state;
const color = theme.sidebarHeaderTextColor;
if (loading) {
return (
<Loading
color={color}
size='small'
style={{
alignItems: 'center',
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
paddingHorizontal: 20
}}
/>
);
}
return (
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center', flex: 1}}>
<TouchableOpacity
onPress={this.onPress}
style={{paddingHorizontal: 15}}
>
<FormattedText
id='add_command.save'
defaultMessage='Save'
style={{color}}
/>
</TouchableOpacity>
</View>
);
}
}
function mapStateToProps(state) {
return {
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(AccountNotifcationsButton);

View file

@ -1,73 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {InteractionManager} from 'react-native';
import ChannelDrawerList from 'app/components/channel_drawer_list';
export default class ChannelDrawer extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
closeDrawers: PropTypes.func.isRequired,
handleSelectChannel: PropTypes.func.isRequired,
viewChannel: PropTypes.func.isRequired,
markChannelAsRead: PropTypes.func.isRequired,
setChannelLoading: PropTypes.func.isRequired
}).isRequired,
currentTeam: PropTypes.object,
currentChannel: PropTypes.object,
channels: PropTypes.object,
channelMembers: PropTypes.object,
theme: PropTypes.object.isRequired
};
static defaultProps = {
currentTeam: {},
currentChannel: {}
};
selectChannel = (id) => {
const {
actions,
currentChannel
} = this.props;
const {
closeDrawers,
handleSelectChannel,
markChannelAsRead,
setChannelLoading,
viewChannel
} = actions;
markChannelAsRead(id, currentChannel.id);
setChannelLoading();
viewChannel(id);
closeDrawers();
InteractionManager.runAfterInteractions(() => {
handleSelectChannel(id);
});
};
render() {
const {
currentChannel,
currentTeam,
channels,
channelMembers,
theme
} = this.props;
return (
<ChannelDrawerList
currentTeam={currentTeam}
currentChannel={currentChannel}
channels={channels}
channelMembers={channelMembers}
theme={theme}
onSelectChannel={this.selectChannel}
/>
);
}
}

View file

@ -1,44 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import {View} from 'react-native';
import {getCurrentChannel, canManageChannelMembers} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'app/selectors/preferences';
import FormattedText from 'app/components/formatted_text';
function ChannelMembersTitle(props) {
const {canManageUsers} = props;
return (
<View style={{alignItems: 'center', justifyContent: 'center', flex: 1, marginHorizontal: 50}}>
<FormattedText
id={canManageUsers ? 'channel_header.manageMembers' : 'channel_header.viewMembers'}
defaultMessage={canManageUsers ? 'Manage Members' : 'View Members'}
style={{color: props.theme.sidebarHeaderTextColor, fontSize: 15, fontWeight: 'bold', textAlign: 'center'}}
/>
</View>
);
}
ChannelMembersTitle.propTypes = {
canManageUsers: PropTypes.bool.isRequired,
currentChannel: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
ChannelMembersTitle.defaultProps = {
theme: {}
};
function mapStateToProps(state) {
return {
canManageUsers: canManageChannelMembers(state),
currentChannel: getCurrentChannel(state),
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(ChannelMembersTitle);

View file

@ -1,41 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import ActionButton from 'app/components/action_button';
import {canManageChannelMembers} from 'mattermost-redux/selectors/entities/channels';
function RemoveMemberButton(props) {
const {canManageUsers} = props;
if (!canManageUsers) {
return null;
}
return (
<ActionButton
actionEventName='remove_members'
emitter={props.emitter}
enabled={false}
enableEventName='can_remove_members'
labelDefaultMessage='Remove'
labelId='channel_members_modal.remove'
loadingEventName='removing_members'
/>
);
}
RemoveMemberButton.propTypes = {
emitter: PropTypes.func.isRequired,
canManageUsers: PropTypes.bool.isRequired
};
function mapStateToProps(state) {
return {
canManageUsers: canManageChannelMembers(state)
};
}
export default connect(mapStateToProps)(RemoveMemberButton);

View file

@ -1,371 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {
Dimensions,
Keyboard,
Platform,
StyleSheet,
TouchableOpacity,
TouchableWithoutFeedback,
View,
findNodeHandle
} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {General, RequestStatus} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import ActionButton from 'app/components/action_button';
class CreateChannel extends PureComponent {
static propTypes = {
intl: intlShape.isRequired,
createChannelRequest: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
channelType: PropTypes.string,
subscribeToHeaderEvent: React.PropTypes.func.isRequired,
unsubscribeFromHeaderEvent: React.PropTypes.func.isRequired,
actions: PropTypes.shape({
goBack: PropTypes.func.isRequired,
closeModal: PropTypes.func.isRequired,
handleCreateChannel: PropTypes.func.isRequired
})
};
static defaultProps = {
channelType: General.OPEN_CHANNEL
};
static navigationProps = {
renderLeftComponent: (props, emitter, theme) => {
return (
<TouchableOpacity
style={{flex: 1, paddingHorizontal: 15, justifyContent: 'center'}}
onPress={() => emitter('close')}
>
<FormattedText
id='more_direct_channels.close'
defaultMessage='Close'
style={{color: theme.sidebarHeaderTextColor}}
/>
</TouchableOpacity>
);
},
renderRightComponent: (props, emitter) => {
return (
<ActionButton
actionEventName='create_channel'
emitter={emitter}
enabled={false}
enableEventName='can_create_channel'
labelDefaultMessage='Create'
labelId='mobile.create_channel'
loadingEventName='creating_channel'
/>
);
}
};
constructor(props) {
super(props);
this.state = {
displayName: '',
header: '',
purpose: ''
};
}
onCreateChannel = async () => {
Keyboard.dismiss();
const {displayName, purpose, header} = this.state;
await this.props.actions.handleCreateChannel(displayName, purpose, header, this.props.channelType);
};
onDisplayNameChangeText = (displayName) => {
this.setState({displayName});
if (displayName && displayName.length >= 2) {
this.emitCanCreateChannel(true);
} else {
this.emitCanCreateChannel(false);
}
};
onPurposeChangeText = (purpose) => {
this.setState({purpose});
};
onHeaderChangeText = (header) => {
this.setState({header});
};
emitCanCreateChannel = (enabled) => {
EventEmitter.emit('can_create_channel', enabled);
};
emitCreating = (loading) => {
EventEmitter.emit('creating_channel', loading);
};
blur = () => {
this.nameInput.refs.wrappedInstance.blur();
this.purposeInput.refs.wrappedInstance.blur();
this.headerInput.refs.wrappedInstance.blur();
this.scroll.scrollToPosition(0, 0, true);
};
channelNameRef = (ref) => {
this.nameInput = ref;
};
channelPurposeRef = (ref) => {
this.purposeInput = ref;
};
channelHeaderRef = (ref) => {
this.headerInput = ref;
};
scrollRef = (ref) => {
this.scroll = ref;
};
lastTextRef = (ref) => {
this.lastText = ref;
};
scrollToEnd = () => {
this.scroll.scrollToFocusedInput(findNodeHandle(this.lastText));
};
componentWillMount() {
this.props.subscribeToHeaderEvent('close', this.props.actions.goBack);
this.props.subscribeToHeaderEvent('create_channel', this.onCreateChannel);
}
componentWillReceiveProps(nextProps) {
const {createChannelRequest} = nextProps;
if (this.props.createChannelRequest !== createChannelRequest) {
switch (createChannelRequest.status) {
case RequestStatus.STARTED:
this.emitCreating(true);
this.setState({error: null});
break;
case RequestStatus.SUCCESS:
this.emitCreating(false);
this.setState({error: null});
this.props.actions.closeModal();
break;
case RequestStatus.FAILURE:
this.emitCreating(false);
this.setState({error: createChannelRequest.error});
break;
}
}
}
componentDidMount() {
this.emitCanCreateChannel(false);
}
componentWillUnmount() {
this.props.unsubscribeFromHeaderEvent('close');
this.props.unsubscribeFromHeaderEvent('create_channel');
}
render() {
const {channelType, theme} = this.props;
const {displayName, header, purpose, error} = this.state;
const {formatMessage} = this.props.intl;
const {height, width} = Dimensions.get('window');
const style = getStyleSheet(theme);
let term;
if (channelType === General.OPEN_CHANNEL) {
term = formatMessage({id: 'channel_modal.channel', defaultMessage: 'Channel'});
} else if (channelType === General.PRIVATE_CHANNEL) {
term = formatMessage({id: 'channel_modal.group', defaultMessage: 'Group'});
}
let displayError;
if (error) {
displayError = (
<View style={[style.errorContainer, {width}]}>
<View style={style.errorWrapper}>
<ErrorText error={error}/>
</View>
</View>
);
}
return (
<KeyboardAwareScrollView
ref={this.scrollRef}
style={style.container}
>
<TouchableWithoutFeedback onPress={this.blur}>
<View style={[style.scrollView, {height: height + (Platform.OS === 'android' ? 200 : 0)}]}>
{displayError}
<View>
<FormattedText
style={[style.title, {marginTop: (error ? 10 : 0)}]}
id='channel_modal.name'
defaultMessage='Name'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelNameRef}
value={displayName}
onChangeText={this.onDisplayNameChangeText}
style={style.input}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'channel_modal.nameEx', defaultMessage: 'E.g.: "Bugs", "Marketing", "客户支持"'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
underlineColorAndroid='transparent'
/>
</View>
<View style={style.titleContainer30}>
<FormattedText
style={style.title}
id='channel_modal.purpose'
defaultMessage='Purpose'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelPurposeRef}
value={purpose}
onChangeText={this.onPurposeChangeText}
style={[style.input, {height: 110}]}
placeholder={{id: 'channel_modal.purposeEx', defaultMessage: 'E.g.: "A channel to file bugs and improvements"'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
multiline={true}
underlineColorAndroid='transparent'
/>
</View>
<View>
<FormattedText
style={style.helpText}
id='channel_modal.descriptionHelp'
defaultMessage='Describe how this {term} should be used.'
values={{
term
}}
/>
</View>
<View style={style.titleContainer15}>
<FormattedText
style={style.title}
id='channel_modal.header'
defaultMessage='Header'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelHeaderRef}
value={header}
onChangeText={this.onHeaderChangeText}
style={[style.input, {height: 110}]}
placeholder={{id: 'channel_modal.headerEx', defaultMessage: 'E.g.: "[Link Title](http://example.com)"'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
multiline={true}
onFocus={this.scrollToEnd}
underlineColorAndroid='transparent'
/>
</View>
<View ref={this.lastTextRef}>
<FormattedText
style={style.helpText}
id='channel_modal.headerHelp'
defaultMessage={'Set text that will appear in the header of the {term} beside the {term} name. For example, include frequently used links by typing [Link Title](http://example.com).'}
values={{
term
}}
/>
</View>
</View>
</TouchableWithoutFeedback>
</KeyboardAwareScrollView>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
paddingTop: 30
},
errorContainer: {
position: 'absolute'
},
errorWrapper: {
justifyContent: 'center',
alignItems: 'center',
marginBottom: 10
},
inputContainer: {
marginTop: 10,
backgroundColor: '#fff'
},
input: {
color: '#333',
fontSize: 14,
height: 40,
paddingHorizontal: 15
},
titleContainer30: {
flexDirection: 'row',
marginTop: 30
},
titleContainer15: {
flexDirection: 'row',
marginTop: 15
},
title: {
fontSize: 14,
color: theme.centerChannelColor,
marginLeft: 15
},
optional: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 14,
marginLeft: 5
},
helpText: {
fontSize: 14,
color: changeOpacity(theme.centerChannelColor, 0.5),
marginTop: 10,
marginHorizontal: 15
}
});
});
export default injectIntl(CreateChannel);

View file

@ -1,58 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import About from './about';
import AccountNotifications from './account_notifications';
import AccountSettings from './account_settings';
import Channel from './channel';
import ChannelDrawer from './channel_drawer';
import ChannelInfo from './channel_info';
import ChannelMembers from './channel_members';
import ChannelAddMembers from './channel_add_members';
import CreateChannel from './create_channel';
import EditPost from './edit_post';
import ImagePreview from './image_preview';
import LoadTeam from './load_team';
import Login from './login';
import LoginOptions from './login_options';
import Mfa from './mfa';
import MoreChannels from './more_channels';
import MoreDirectMessages from './more_dms';
import OptionsModal from './options_modal';
import Settings from './settings';
import Root from './root';
import Search from './search';
import SelectServer from './select_server';
import SelectTeam from './select_team';
import Thread from './thread';
import UserProfile from './user_profile';
import Saml from './saml';
module.exports = {
About,
AccountNotifications,
AccountSettings,
ChannelView: Channel, // Special case the name for this one to avoid ambiguity
ChannelDrawer,
ChannelInfo,
ChannelMembers,
ChannelAddMembers,
CreateChannel,
EditPost,
ImagePreview,
LoadTeam,
Login,
LoginOptions,
Mfa,
MoreChannels,
MoreDirectMessages,
OptionsModal,
Settings,
Root,
Search,
SelectServer,
SelectTeam,
Thread,
UserProfile,
Saml
};

View file

@ -1,29 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import navigationSceneConnect from '../navigationSceneConnect';
import {goToLogin, goToSaml} from 'app/actions/navigation';
import LoginOptions from './login_options';
function mapStateToProps(state) {
const {config, license} = state.entities.general;
return {
config,
license
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToLogin,
goToSaml
}, dispatch)
};
}
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(LoginOptions);

View file

@ -1,54 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import ActionButton from 'app/components/action_button';
import {getTheme} from 'app/selectors/preferences';
import {General} from 'mattermost-redux/constants';
import {getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {showCreateOption} from 'mattermost-redux/utils/channel_utils';
import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
function CreateButton(props) {
if (!props.canCreateChannels) {
return null;
}
return (
<ActionButton
actionEventName='create_channel'
emitter={props.emitter}
enabled={true}
enableEventName='can_create_channel'
labelDefaultMessage='Create'
labelId='mobile.create_channel'
loadingEventName='creating_channel'
/>
);
}
CreateButton.propTypes = {
canCreateChannels: PropTypes.bool.isRequired,
emitter: PropTypes.func.isRequired,
theme: PropTypes.object
};
CreateButton.defaultProps = {
theme: {}
};
function mapStateToProps(state) {
const {config, license} = state.entities.general;
const roles = getCurrentUserRoles(state);
return {
canCreateChannels: showCreateOption(config, license, General.PRIVATE_CHANNEL, isAdmin(roles), isSystemAdmin(roles)),
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(CreateButton);

View file

@ -1,69 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
/* eslint-disable */
import {connect} from 'react-redux';
import React, {PropTypes} from 'react';
import {
Platform,
TouchableOpacity,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import FormattedText from 'app/components/formatted_text';
const defaults = {
renderBackButton: (props, emitter, theme) => {
return (
<TouchableOpacity
style={{flexDirection: 'row', flex: 1, paddingHorizontal: 15, alignItems: 'center'}}
onPress={props.onNavigateBack}
>
<Icon
style={{fontWeight: 'bold'}}
name='angle-left'
size={35}
color={theme.sidebarHeaderTextColor}
/>
</TouchableOpacity>
);
},
renderTitleComponent: (props, emitter, theme) => {
const navProps = props.scene.route.navigationProps || {};
const title = navProps.title;
if (title) {
return (
<View style={{alignItems: 'center', justifyContent: 'center', flex: 1, marginHorizontal: 50}}>
<FormattedText
id={title.id}
defaultMessage={title.defaultMessage}
ellipsizeMode='tail'
numberOfLines={1}
style={{
textAlign: 'center',
color: theme.sidebarHeaderTextColor,
fontSize: 15,
fontWeight: 'bold'
}}
/>
</View>
);
}
},
headerStyle: {
flexDirection: 'row',
alignItems: 'center'
},
allowSceneSwipe: true,
allowMenuSwipe: false
};
export default (stateProps, dispatchProps) => (WrappedComponent) => {
const componentNavigationProps = WrappedComponent.WrappedComponent ? WrappedComponent.WrappedComponent.navigationProps : WrappedComponent.navigationProps;
WrappedComponent.navigationProps = Object.assign({}, defaults, componentNavigationProps);
return connect(stateProps, dispatchProps)(WrappedComponent);
};

View file

@ -1,27 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {closeModal} from 'app/actions/navigation';
import navigationSceneConnect from '../navigationSceneConnect';
import OptionsModal from './options_modal';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
requestClose: state.navigation.modal.requestClose
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
closeModal
}, dispatch)
};
}
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(OptionsModal);

View file

@ -1,37 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import navigationSceneConnect from '../navigationSceneConnect';
import {goToSelectServer} from 'app/actions/views/root';
import {handleServerUrlChanged} from 'app/actions/views/select_server';
import {goToLoadTeam} from 'app/actions/navigation';
import {loadMe} from 'mattermost-redux/actions/users';
import Root from './root';
function mapStateToProps(state, ownProps) {
return {
...ownProps,
credentials: state.entities.general.credentials,
hydrationComplete: state.views.root.hydrationComplete,
logoutRequest: state.requests.users.logout,
loginRequest: state.requests.users.login
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToLoadTeam,
goToSelectServer,
handleServerUrlChanged,
loadMe
}, dispatch)
};
}
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(Root);

View file

@ -1,80 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import Orientation from 'react-native-orientation';
import SplashScreen from 'react-native-smart-splash-screen';
import {Client, Client4} from 'mattermost-redux/client';
import {RequestStatus} from 'mattermost-redux/constants';
import Loading from 'app/components/loading';
import {stripTrailingSlashes} from 'app/utils/url';
export default class Root extends PureComponent {
static propTypes = {
credentials: PropTypes.object,
logoutRequest: PropTypes.object,
loginRequest: PropTypes.object,
actions: PropTypes.shape({
goToLoadTeam: PropTypes.func,
goToSelectServer: PropTypes.func,
handleServerUrlChanged: PropTypes.func.isRequired,
loadMe: PropTypes.func
}).isRequired
};
static navigationProps = {
hideNavBar: true
};
componentDidMount() {
Orientation.lockToPortrait();
this.loadStoreAndScene(this.props.credentials);
setTimeout(() => {
this.handleCloseSplashScreen();
}, 1000);
}
componentWillReceiveProps(nextProps) {
if (this.props.logoutRequest.status === RequestStatus.SUCCESS &&
nextProps.logoutRequest.status === RequestStatus.NOT_STARTED) {
this.loadStoreAndScene(nextProps.credentials);
}
}
handleCloseSplashScreen = (options = {}) => {
const opts = {
animationType: SplashScreen.animationType.scale,
duration: 850,
delay: 500
};
SplashScreen.close(Object.assign({}, opts, options));
};
loadStoreAndScene = (credentials = {}) => {
const {actions, loginRequest} = this.props;
const {goToLoadTeam, goToSelectServer, loadMe} = actions;
if (credentials.token && credentials.url) {
// Will probably need to make this optimistic since we
// assume that the stored token is good.
if (loginRequest.status === RequestStatus.NOT_STARTED) {
Client4.setToken(credentials.token);
Client4.setUrl(stripTrailingSlashes(credentials.url));
Client.setUrl(stripTrailingSlashes(credentials.url));
loadMe().then(goToLoadTeam).catch(goToLoadTeam);
} else {
goToLoadTeam();
}
} else {
goToSelectServer();
}
};
render() {
return <Loading/>;
}
}

View file

@ -1,30 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import Button from 'app/components/button';
import {GlobalStyles} from 'app/styles';
import {Text, View} from 'react-native';
export default class Search extends React.Component {
static propTypes = {
actions: React.PropTypes.object.isRequired,
searchType: React.PropTypes.string.isRequired
};
render() {
return (
<View style={GlobalStyles.container}>
<Text>
{'This is a search for ' + this.props.searchType}
</Text>
<Button onPress={this.props.actions.goBack}>
<Text>
{'<< Go back'}
</Text>
</Button>
</View>
);
}
}

View file

@ -1,66 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import {
View
} from 'react-native';
import {General} from 'mattermost-redux/constants';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'app/selectors/preferences';
import FormattedText from 'app/components/formatted_text';
function ThreadTitle(props) {
const {currentChannel, theme} = props;
let label;
if (currentChannel.type === General.DM_CHANNEL) {
label = (
<FormattedText
id='mobile.routes.thread_dm'
defaultMessage='Direct Message Thread'
ellipsizeMode='tail'
numberOfLines={1}
style={{color: theme.sidebarHeaderTextColor, fontSize: 15, fontWeight: 'bold'}}
/>
);
} else {
const channelName = currentChannel.display_name;
label = (
<FormattedText
id='mobile.routes.thread'
defaultMessage='{channelName} Thread'
ellipsizeMode='tail'
numberOfLines={1}
values={{channelName}}
style={{color: theme.sidebarHeaderTextColor, fontSize: 15, fontWeight: 'bold'}}
/>
);
}
return (
<View style={{justifyContent: 'center', flex: 1, marginHorizontal: 1}}>
{label}
</View>
);
}
ThreadTitle.propTypes = {
currentChannel: PropTypes.object.isRequired,
theme: PropTypes.object
};
ThreadTitle.defaultProps = {
theme: {}
};
function mapStateToProps(state) {
return {
currentChannel: getCurrentChannel(state),
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(ThreadTitle);

View file

@ -1,8 +1,9 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'app/selectors/preferences';
import navigationSceneConnect from 'app/scenes/navigationSceneConnect';
import About from './about';
@ -16,4 +17,4 @@ function mapStateToProps(state) {
};
}
export default navigationSceneConnect(mapStateToProps)(About);
export default connect(mapStateToProps)(About);

View file

@ -2,43 +2,43 @@
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {
Keyboard,
Platform,
ScrollView,
StatusBar,
StyleSheet,
View
} from 'react-native';
import {Preferences, RequestStatus} from 'mattermost-redux/constants';
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
import Loading from 'app/components/loading';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {Preferences} from 'mattermost-redux/constants';
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
import Section from './section';
import SectionItem from './section_item';
import SaveNotificationsButton from './save_notifications_button';
const SAVE_NOTIFY_PROPS = 'save_notify_props';
export default class AccountNotifications extends PureComponent {
class AccountNotifications extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
goBack: PropTypes.func.isRequired,
handleUpdateUserNotifyProps: PropTypes.func.isRequired
}),
config: PropTypes.object.isRequired,
currentUser: PropTypes.object.isRequired,
intl: intlShape.isRequired,
myPreferences: PropTypes.object.isRequired,
subscribeToHeaderEvent: PropTypes.func.isRequired,
navigator: PropTypes.object,
theme: PropTypes.object.isRequired,
unsubscribeFromHeaderEvent: PropTypes.func.isRequired
updateMeRequest: PropTypes.object.isRequired
};
static navigationProps = {
renderRightComponent: (props, emitter) => {
return <SaveNotificationsButton emitter={emitter}/>;
}
saveButton = {
id: 'save-notifications',
showAsAction: 'never'
};
constructor(props) {
@ -46,11 +46,16 @@ export default class AccountNotifications extends PureComponent {
const {currentUser} = props;
const notifyProps = currentUser.notify_props || {};
this.setStateFromNotifyProps(notifyProps);
}
componentWillMount() {
this.props.subscribeToHeaderEvent(SAVE_NOTIFY_PROPS, this.saveUserNotifyProps);
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
props.navigator.setButtons({
rightButtons: [{
...this.saveButton,
title: props.intl.formatMessage({id: 'edit_post.save', defaultMessage: 'Save'})
}]
});
this.setStateFromNotifyProps(notifyProps);
}
componentDidMount() {
@ -60,24 +65,49 @@ export default class AccountNotifications extends PureComponent {
}
componentWillUnmount() {
this.props.unsubscribeFromHeaderEvent(SAVE_NOTIFY_PROPS);
if (Platform.OS === 'android') {
Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard);
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.currentUser !== this.props.currentUser) {
const {notify_props: notifyProps} = nextProps.currentUser;
const {currentUser, updateMeRequest} = nextProps;
if (currentUser !== this.props.currentUser) {
const {notify_props: notifyProps} = currentUser;
this.setStateFromNotifyProps(notifyProps);
}
if (this.props.updateMeRequest !== updateMeRequest) {
switch (updateMeRequest.status) {
case RequestStatus.STARTED:
this.savingNotifyProps(true);
this.setState({error: null, saving: true});
break;
case RequestStatus.SUCCESS:
this.savingNotifyProps(false);
this.setState({error: null, saving: false});
this.close();
break;
case RequestStatus.FAILURE:
this.savingNotifyProps(false);
this.setState({error: updateMeRequest.error, saving: false});
break;
}
}
}
handleAndroidKeyboard = () => {
this.refs.mention_keys.getWrappedInstance().blur();
};
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'save-notifications') {
this.saveUserNotifyProps();
}
}
};
setStateFromNotifyProps = (notifyProps) => {
const mentionKeys = (notifyProps.mention_keys || '').split(',');
const usernameMentionIndex = mentionKeys.indexOf(this.props.currentUser.username);
@ -108,31 +138,37 @@ export default class AccountNotifications extends PureComponent {
} else {
this.state = {...newState};
}
}
};
toggleFirstNameMention = () => {
this.setState({
first_name: (!(this.state.first_name === 'true')).toString()
});
}
};
toggleUsernameMention = () => {
this.setState({
usernameMention: !this.state.usernameMention
});
}
};
toggleChannelMentions = () => {
this.setState({
channel: (!(this.state.channel === 'true')).toString()
});
}
};
updateMentionKeys = (text) => {
this.setState({
mention_keys: text
});
}
};
savingNotifyProps = (loading) => {
this.props.navigator.setButtons({
rightButtons: [{...this.saveButton, disabled: loading}]
});
};
setEmailNotifications = (value) => {
const {config} = this.props;
@ -149,25 +185,25 @@ export default class AccountNotifications extends PureComponent {
email,
interval
});
}
};
setReplyNotifications = (value) => {
this.setState({
comments: value
});
}
};
setMobilePush = (value) => {
this.setState({
push: value
});
}
};
setMobilePushStatus = (value) => {
this.setState({
push_status: value
});
}
};
saveUserNotifyProps = () => {
let {mention_keys: mentionKeys, usernameMention, ...notifyProps} = this.state; //eslint-disable-line prefer-const
@ -189,9 +225,7 @@ export default class AccountNotifications extends PureComponent {
mention_keys: mentionKeys,
user_id: this.props.currentUser.id
});
this.props.actions.goBack();
}
};
buildMentionSection = () => {
const {currentUser, theme} = this.props;
@ -258,7 +292,7 @@ export default class AccountNotifications extends PureComponent {
</SectionItem>
</Section>
);
}
};
buildEmailSection = () => {
const {config, theme} = this.props;
@ -348,7 +382,7 @@ export default class AccountNotifications extends PureComponent {
}
</Section>
);
}
};
buildReplySection = () => {
const {theme} = this.props;
@ -393,7 +427,7 @@ export default class AccountNotifications extends PureComponent {
/>
</Section>
);
}
};
buildMobilePushSection = () => {
const {config, theme} = this.props;
@ -443,7 +477,7 @@ export default class AccountNotifications extends PureComponent {
/>
</Section>
);
}
};
buildMobilePushStatusSection = () => {
const {config, theme} = this.props;
@ -493,14 +527,28 @@ export default class AccountNotifications extends PureComponent {
/>
</Section>
);
}
};
close = () => {
this.props.navigator.pop({animated: true});
};
render() {
const {theme} = this.props;
const style = getStyleSheet(theme);
if (this.state.saving) {
return (
<View style={style.wrapper}>
<StatusBar barStyle='light-content'/>
<Loading/>
</View>
);
}
return (
<View style={style.wrapper}>
<StatusBar barStyle='light-content'/>
<ScrollView
style={style.scrollView}
contentContainerStyle={style.scrollViewContent}
@ -518,6 +566,10 @@ export default class AccountNotifications extends PureComponent {
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
wrapper: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
input: {
color: theme.centerChannelColor,
fontSize: 12,
@ -535,10 +587,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
scrollViewContent: {
paddingBottom: 30
},
wrapper: {
flex: 1,
backgroundColor: theme.centerChannelBg
}
});
});
export default injectIntl(AccountNotifications);

View file

@ -2,21 +2,22 @@
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goBack} from 'app/actions/navigation';
import {handleUpdateUserNotifyProps} from 'app/actions/views/account_notifications';
import {getTheme} from 'app/selectors/preferences';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import navigationSceneConnect from '../navigationSceneConnect';
import {handleUpdateUserNotifyProps} from 'app/actions/views/account_notifications';
import {getTheme} from 'app/selectors/preferences';
import AccountNotifications from './account_notifications';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
return {
...ownProps,
config: state.entities.general.config,
myPreferences: state.entities.preferences.myPreferences,
currentUser: getCurrentUser(state),
updateMeRequest: state.requests.users.updateMe,
theme: getTheme(state)
};
}
@ -24,10 +25,9 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goBack,
handleUpdateUserNotifyProps
}, dispatch)
};
}
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(AccountNotifications);
export default connect(mapStateToProps, mapDispatchToProps)(AccountNotifications);

View file

@ -2,8 +2,10 @@
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {
StyleSheet,
StatusBar,
TouchableOpacity,
View
} from 'react-native';
@ -13,31 +15,13 @@ import FormattedText from 'app/components/formatted_text';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class AccountSettings extends PureComponent {
class AccountSettings extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
goToAccountNotifications: PropTypes.func.isRequired
}),
intl: intlShape.isRequired,
navigator: PropTypes.object,
theme: PropTypes.object.isRequired
};
static navigationProps = {
renderLeftComponent: (props, emitter, theme) => {
return (
<TouchableOpacity
style={{flex: 1, paddingHorizontal: 15, justifyContent: 'center'}}
onPress={props.onNavigateBack}
>
<FormattedText
id='admin.select_team.close'
defaultMessage='Close'
style={{color: theme.sidebarHeaderTextColor}}
/>
</TouchableOpacity>
);
}
};
// The enabled arg can be removed once all the scenes have been implemented.
buildItemRow = (icon, id, defaultMessage, action, separator = true, enabled = false) => {
if (!enabled) {
@ -79,11 +63,27 @@ export default class AccountSettings extends PureComponent {
);
};
goToAccountNotifications = () => {
const {intl, navigator, theme} = this.props;
navigator.push({
backButtonTitle: '',
screen: 'AccountNotifications',
title: intl.formatMessage({id: 'user.settings.modal.notifications', defaultMessage: 'Notifications'}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
}
});
};
renderItems = () => {
return [
this.buildItemRow('gear', 'user.settings.modal.general', 'General', () => true, true, false),
this.buildItemRow('lock', 'user.settings.modal.security', 'Security', () => true, true, false),
this.buildItemRow('bell', 'user.settings.modal.notifications', 'Notifications', this.props.actions.goToAccountNotifications, false, true),
this.buildItemRow('bell', 'user.settings.modal.notifications', 'Notifications', this.goToAccountNotifications, false, true),
this.buildItemRow('mobile', 'user.settings.modal.display', 'Display', () => true, true, false),
this.buildItemRow('wrench', 'user.settings.modal.advanced', 'Advanced', () => true, false, false)
];
@ -95,6 +95,7 @@ export default class AccountSettings extends PureComponent {
return (
<View style={style.wrapper}>
<StatusBar barStyle='light-content'/>
<View style={style.container}>
<View style={style.itemsContainer}>
{this.renderItems()}
@ -154,3 +155,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
}
});
});
export default injectIntl(AccountSettings);

View file

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

View file

@ -2,27 +2,32 @@
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {
Dimensions,
NetInfo,
Platform,
StatusBar,
StyleSheet,
View
} from 'react-native';
import {RequestStatus} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import ChannelDrawer from 'app/components/channel_drawer';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import Loading from 'app/components/loading';
import OfflineIndicator from 'app/components/offline_indicator';
import PostTextbox from 'app/components/post_textbox';
import {preventDoubleTap} from 'app/utils/tap';
import {RequestStatus} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import ChannelDrawerButton from './channel_drawer_button';
import ChannelTitle from './channel_title';
import ChannelPostList from './channel_post_list';
import ChannelPostList from './channel_post_list/index';
export default class Channel extends PureComponent {
class Channel extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
connection: PropTypes.func.isRequired,
@ -30,15 +35,14 @@ export default class Channel extends PureComponent {
loadProfilesAndTeamMembersForDMSidebar: PropTypes.func.isRequired,
selectFirstAvailableTeam: PropTypes.func.isRequired,
selectInitialChannel: PropTypes.func.isRequired,
openChannelDrawer: PropTypes.func.isRequired,
handlePostDraftChanged: PropTypes.func.isRequired,
goToChannelInfo: PropTypes.func.isRequired,
initWebSocket: PropTypes.func.isRequired,
closeWebSocket: PropTypes.func.isRequired,
startPeriodicStatusUpdates: PropTypes.func.isRequired,
stopPeriodicStatusUpdates: PropTypes.func.isRequired,
renderDrawer: PropTypes.func.isRequired
stopPeriodicStatusUpdates: PropTypes.func.isRequired
}).isRequired,
intl: intlShape.isRequired,
navigator: PropTypes.object,
appState: PropTypes.bool,
currentTeam: PropTypes.object,
currentChannel: PropTypes.object,
@ -49,21 +53,9 @@ export default class Channel extends PureComponent {
webSocketRequest: PropTypes.object
};
static navigationProps = {
allowMenuSwipe: true,
renderLeftComponent: (props, emitter) => {
return <ChannelDrawerButton emitter={emitter}/>;
},
renderTitleComponent: (props, emitter) => {
return <ChannelTitle emitter={emitter}/>;
}
};
componentWillMount() {
this.props.subscribeToHeaderEvent('open_channel_drawer', () => preventDoubleTap(this.openChannelDrawer, this));
this.props.subscribeToHeaderEvent('show_channel_info', () => preventDoubleTap(this.props.actions.goToChannelInfo));
NetInfo.isConnected.addEventListener('change', this.handleConnectionChange);
EventEmitter.on('leave_team', this.handleLeaveTeam);
NetInfo.isConnected.addEventListener('change', this.handleConnectionChange);
NetInfo.isConnected.fetch().then(this.handleConnectionChange);
if (this.props.currentTeam) {
@ -73,13 +65,13 @@ export default class Channel extends PureComponent {
}
componentDidMount() {
const {renderDrawer, startPeriodicStatusUpdates} = this.props.actions;
const {startPeriodicStatusUpdates} = this.props.actions;
try {
startPeriodicStatusUpdates();
} catch (error) {
// We don't care about the error
}
renderDrawer();
}
componentWillReceiveProps(nextProps) {
@ -90,13 +82,40 @@ export default class Channel extends PureComponent {
}
componentWillUnmount() {
this.props.actions.closeWebSocket();
this.props.actions.stopPeriodicStatusUpdates();
this.props.unsubscribeFromHeaderEvent('open_channel_drawer');
NetInfo.isConnected.removeEventListener('change', this.handleConnectionChange);
const {closeWebSocket, stopPeriodicStatusUpdates} = this.props.actions;
EventEmitter.off('leave_team', this.handleLeaveTeam);
NetInfo.isConnected.removeEventListener('change', this.handleConnectionChange);
closeWebSocket();
stopPeriodicStatusUpdates();
}
attachPostTextbox = (ref) => {
this.postTextbox = ref;
};
blurPostTextBox = () => {
this.postTextbox.getWrappedInstance().getWrappedInstance().blur();
};
goToChannelInfo = () => {
const {intl, navigator, theme} = this.props;
navigator.push({
screen: 'ChannelInfo',
title: intl.formatMessage({id: 'mobile.routes.channelInfo', defaultMessage: 'Info'}),
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
}
});
};
handleConnectionChange = (isConnected) => {
const {actions, webSocketRequest} = this.props;
const {closeWebSocket, connection, initWebSocket} = actions;
@ -115,27 +134,6 @@ export default class Channel extends PureComponent {
connection(isConnected);
};
loadChannels = (teamId) => {
const {
loadChannelsIfNecessary,
loadProfilesAndTeamMembersForDMSidebar,
selectInitialChannel
} = this.props.actions;
loadChannelsIfNecessary(teamId).then(() => {
loadProfilesAndTeamMembersForDMSidebar(teamId);
return selectInitialChannel(teamId);
});
};
openChannelDrawer = () => {
this.postTextbox.getWrappedInstance().getWrappedInstance().blur();
this.props.actions.openChannelDrawer();
};
attachPostTextbox = (ref) => {
this.postTextbox = ref;
};
handleDraftChanged = (value) => {
this.props.actions.handlePostDraftChanged(this.props.currentChannel.id, value);
};
@ -144,10 +142,26 @@ export default class Channel extends PureComponent {
this.props.actions.selectFirstAvailableTeam();
};
loadChannels = (teamId) => {
const {
loadChannelsIfNecessary,
loadProfilesAndTeamMembersForDMSidebar,
selectInitialChannel
} = this.props.actions;
loadChannelsIfNecessary(teamId).then(() => {
loadProfilesAndTeamMembersForDMSidebar(teamId);
return selectInitialChannel(teamId);
}).catch(() => {
return selectInitialChannel(teamId);
});
};
render() {
const {
currentTeam,
currentChannel,
navigator,
theme
} = this.props;
@ -159,25 +173,67 @@ export default class Channel extends PureComponent {
);
}
const channelDraft = this.props.drafts[this.props.currentChannel.id];
const channelDraft = this.props.drafts[currentChannel.id];
const style = getStyleFromTheme(theme);
return (
<KeyboardLayout
behavior='padding'
style={{flex: 1, backgroundColor: theme.centerChannelBg}}
keyboardVerticalOffset={65}
<ChannelDrawer
blurPostTextBox={this.blurPostTextBox}
navigator={navigator}
>
<StatusBar barStyle='light-content'/>
<OfflineIndicator/>
<ChannelPostList channel={currentChannel}/>
<PostTextbox
ref={this.attachPostTextbox}
files={channelDraft.files}
value={channelDraft.draft}
channelId={currentChannel.id}
onChangeText={this.handleDraftChanged}
/>
</KeyboardLayout>
<KeyboardLayout
behavior='padding'
style={{flex: 1, backgroundColor: theme.centerChannelBg}}
keyboardVerticalOffset={0}
>
<ChannelPostList
channel={currentChannel}
navigator={navigator}
/>
<PostTextbox
ref={this.attachPostTextbox}
files={channelDraft.files}
value={channelDraft.draft}
channelId={currentChannel.id}
onChangeText={this.handleDraftChanged}
navigator={navigator}
/>
</KeyboardLayout>
<View style={{flex: 1, position: 'absolute'}}>
<View style={style.header}>
<ChannelDrawerButton/>
<ChannelTitle
onPress={() => preventDoubleTap(this.goToChannelInfo, this)}
/>
</View>
<OfflineIndicator/>
</View>
</ChannelDrawer>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
header: {
backgroundColor: theme.sidebarHeaderBg,
flexDirection: 'row',
justifyContent: 'flex-start',
width: Dimensions.get('window').width,
zIndex: 10,
elevation: 2,
...Platform.select({
android: {
height: 46
},
ios: {
height: 64,
paddingTop: 20
}
})
}
});
});
export default injectIntl(Channel);

View file

@ -10,10 +10,12 @@ import {
TouchableOpacity,
View
} from 'react-native';
import DeviceNotification from 'react-native-push-notification';
import Icon from 'react-native-vector-icons/Ionicons';
import Badge from 'app/components/badge';
import {getTheme} from 'app/selectors/preferences';
import {preventDoubleTap} from 'app/utils/tap';
import {getUnreads} from 'mattermost-redux/selectors/entities/channels';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
@ -21,7 +23,6 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
class ChannelDrawerButton extends PureComponent {
static propTypes = {
applicationInitializing: PropTypes.bool.isRequired,
emitter: PropTypes.func.isRequired,
theme: PropTypes.object,
messageCount: PropTypes.number,
mentionCount: PropTypes.number
@ -56,6 +57,12 @@ class ChannelDrawerButton extends PureComponent {
EventEmitter.on('drawer_opacity', this.setOpacity);
}
componentWillReceiveProps(nextProps) {
if (nextProps.mentionCount !== this.props.mentionCount) {
DeviceNotification.setApplicationIconBadgeNumber(nextProps.mentionCount);
}
}
componentWillUnmount() {
EventEmitter.off('drawer_opacity', this.setOpacity);
}
@ -65,7 +72,7 @@ class ChannelDrawerButton extends PureComponent {
};
handlePress = () => {
this.props.emitter('open_channel_drawer');
EventEmitter.emit('open_channel_drawer');
};
render() {
@ -88,7 +95,7 @@ class ChannelDrawerButton extends PureComponent {
count={badgeCount}
minHeight={5}
minWidth={5}
onPress={this.handlePress}
onPress={() => preventDoubleTap(this.handlePress, this)}
/>
);
}
@ -114,7 +121,7 @@ class ChannelDrawerButton extends PureComponent {
const style = StyleSheet.create({
container: {
flex: 1
width: 40
},
wrapper: {
alignItems: 'center',
@ -151,7 +158,7 @@ const style = StyleSheet.create({
function mapStateToProps(state) {
return {
applicationInitializing: state.views.channel.appInitializing,
applicationInitializing: state.views.root.appInitializing,
theme: getTheme(state),
...getUnreads(state)
};

View file

@ -2,6 +2,7 @@
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {
Animated,
Dimensions,
@ -17,17 +18,19 @@ import PostListRetry from 'app/components/post_list_retry';
const {View: AnimatedView} = Animated;
const {height: deviceHeight, width: deviceWidth} = Dimensions.get('window');
export default class ChannelPostList extends PureComponent {
class ChannelPostList extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
loadPostsIfNecessary: PropTypes.func.isRequired,
getPostsBefore: PropTypes.func.isRequired,
goToThread: PropTypes.func.isRequired
selectPost: PropTypes.func.isRequired
}).isRequired,
applicationInitializing: PropTypes.bool.isRequired,
channel: PropTypes.object.isRequired,
channelIsLoading: PropTypes.bool,
intl: intlShape.isRequired,
myMember: PropTypes.object.isRequired,
navigator: PropTypes.object,
postsRequests: PropTypes.shape({
getPosts: PropTypes.object.isRequired,
getPostsBefore: PropTypes.object.isRequired,
@ -105,7 +108,36 @@ export default class ChannelPostList extends PureComponent {
};
goToThread = (post) => {
this.props.actions.goToThread(post.channel_id, post.root_id || post.id);
const {actions, channel, intl, navigator, theme} = this.props;
const channelId = post.channel_id;
const rootId = (post.root_id || post.id);
actions.selectPost(rootId);
let title;
if (channel.type === General.DM_CHANNEL) {
title = intl.formatMessage({id: 'mobile.routes.thread_dm', defaultMessage: 'Direct Message Thread'});
} else {
const channelName = channel.display_name;
title = intl.formatMessage({id: 'mobile.routes.thread', defaultMessage: '{channelName} Thread'}, {channelName});
}
navigator.push({
screen: 'Thread',
title,
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
},
passProps: {
channelId,
rootId
}
});
};
render() {
@ -114,6 +146,7 @@ export default class ChannelPostList extends PureComponent {
applicationInitializing,
channel,
channelIsLoading,
navigator,
posts,
postsRequests,
theme,
@ -141,6 +174,7 @@ export default class ChannelPostList extends PureComponent {
currentUserId={this.props.myMember.user_id}
lastViewedAt={this.state.lastViewedAt}
channel={channel}
navigator={navigator}
/>
);
} else {
@ -167,3 +201,5 @@ export default class ChannelPostList extends PureComponent {
);
}
}
export default injectIntl(ChannelPostList);

View file

@ -4,11 +4,10 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {createSelector} from 'reselect';
import {getPostsBefore} from 'mattermost-redux/actions/posts';
import {selectPost, getPostsBefore} from 'mattermost-redux/actions/posts';
import {getAllPosts, getPostsInCurrentChannel} from 'mattermost-redux/selectors/entities/posts';
import {getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels';
import {goToThread} from 'app/actions/navigation';
import {loadPostsIfNecessary} from 'app/actions/views/channel';
import {getTheme} from 'app/selectors/preferences';
@ -67,7 +66,7 @@ const getPostsInCurrentChannelWithReplyProps = createSelector(
function mapStateToProps(state, ownProps) {
return {
...ownProps,
applicationInitializing: state.views.channel.appInitializing,
applicationInitializing: state.views.root.appInitializing,
channelIsLoading: state.views.channel.loading,
myMember: getMyCurrentChannelMembership(state),
postsRequests: state.requests.posts,
@ -82,7 +81,7 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
loadPostsIfNecessary,
getPostsBefore,
goToThread
selectPost
}, dispatch)
};
}

View file

@ -11,6 +11,7 @@ import {
import Icon from 'react-native-vector-icons/FontAwesome';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'app/selectors/preferences';
function ChannelTitle(props) {
@ -33,8 +34,8 @@ function ChannelTitle(props) {
return (
<TouchableOpacity
style={{flexDirection: 'row', flex: 1}}
onPress={() => props.emitter('show_channel_info')}
style={{flexDirection: 'row', flex: 1, marginRight: 40}}
onPress={props.onPress}
>
<View style={{flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginHorizontal: 15}}>
<Text
@ -56,7 +57,7 @@ function ChannelTitle(props) {
ChannelTitle.propTypes = {
applicationInitializing: PropTypes.bool.isRequired,
currentChannel: PropTypes.object,
emitter: PropTypes.func.isRequired,
onPress: PropTypes.func,
theme: PropTypes.object
};
@ -67,7 +68,7 @@ ChannelTitle.defaultProps = {
function mapStateToProps(state) {
return {
applicationInitializing: state.views.channel.appInitializing,
applicationInitializing: state.views.root.appInitializing,
currentChannel: getCurrentChannel(state),
theme: getTheme(state)
};

View file

@ -2,14 +2,8 @@
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import navigationSceneConnect from '../navigationSceneConnect';
import {
goToChannelInfo,
openChannelDrawer,
renderDrawer
} from 'app/actions/navigation';
import {
loadChannelsIfNecessary,
loadProfilesAndTeamMembersForDMSidebar,
@ -52,16 +46,13 @@ function mapDispatchToProps(dispatch) {
loadProfilesAndTeamMembersForDMSidebar,
selectFirstAvailableTeam,
selectInitialChannel,
openChannelDrawer,
handlePostDraftChanged,
goToChannelInfo,
initWebSocket,
closeWebSocket,
startPeriodicStatusUpdates,
stopPeriodicStatusUpdates,
renderDrawer
stopPeriodicStatusUpdates
}, dispatch)
};
}
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(Channel);
export default connect(mapStateToProps, mapDispatchToProps)(Channel);

View file

@ -8,18 +8,18 @@ import {
Keyboard,
Platform,
InteractionManager,
StatusBar,
StyleSheet,
View
} from 'react-native';
import ActionButton from 'app/components/action_button';
import Loading from 'app/components/loading';
import MemberList from 'app/components/custom_list';
import SearchBar from 'app/components/search_bar';
import {createMembersSections, loadingText, markSelectedProfiles, renderMemberRow} from 'app/utils/member_list';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {General, RequestStatus} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils';
class ChannelAddMembers extends PureComponent {
@ -31,35 +31,23 @@ class ChannelAddMembers extends PureComponent {
membersNotInChannel: PropTypes.array.isRequired,
currentTeam: PropTypes.object,
currentTeamMemberCount: PropTypes.number,
navigator: PropTypes.object,
preferences: PropTypes.object,
loadMoreRequestStatus: PropTypes.string,
searchRequestStatus: PropTypes.string,
addChannelMemberStatus: PropTypes.string,
subscribeToHeaderEvent: React.PropTypes.func,
unsubscribeFromHeaderEvent: React.PropTypes.func,
actions: PropTypes.shape({
getTeamStats: PropTypes.func.isRequired,
getProfilesNotInChannel: PropTypes.func.isRequired,
goBack: PropTypes.func.isRequired,
handleAddChannelMembers: PropTypes.func.isRequired,
searchProfiles: PropTypes.func.isRequired
})
};
static navigationProps = {
renderRightComponent: (props, emitter) => {
return (
<ActionButton
actionEventName='add_members'
emitter={emitter}
enabled={false}
enableEventName='can_add_members'
labelDefaultMessage='Add'
labelId='integrations.add'
loadingEventName='adding_members'
/>
);
}
addButton = {
disabled: true,
id: 'add-members',
showAsAction: 'never'
};
constructor(props) {
@ -76,10 +64,12 @@ class ChannelAddMembers extends PureComponent {
selectedMembers: {},
showNoResults: false
};
}
this.addButton.title = props.intl.formatMessage({id: 'integrations.add', defaultMessage: 'Add'});
componentWillMount() {
this.props.subscribeToHeaderEvent('add_members', this.handleAddMembersPress);
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
props.navigator.setButtons({
rightButtons: [this.addButton]
});
}
componentDidMount() {
@ -96,8 +86,6 @@ class ChannelAddMembers extends PureComponent {
}
componentWillUnmount() {
this.props.unsubscribeFromHeaderEvent('add_members');
if (Platform.OS === 'android') {
Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard);
}
@ -128,20 +116,46 @@ class ChannelAddMembers extends PureComponent {
switch (addChannelMemberStatus) {
case RequestStatus.STARTED:
this.emitAdding(true);
this.setState({error: null});
this.setState({error: null, adding: true, canSelect: false});
break;
case RequestStatus.SUCCESS:
this.emitAdding(false);
this.setState({error: null});
this.props.actions.goBack();
this.setState({error: null, adding: false, canSelect: false});
this.close();
break;
case RequestStatus.FAILURE:
this.emitAdding(false);
this.setState({adding: false, canSelect: true});
break;
}
}
}
cancelSearch = () => {
this.setState({
searching: false,
term: null,
page: 0,
profiles: markSelectedProfiles(this.props.membersNotInChannel, this.state.selectedMembers)
});
};
close = () => {
this.props.navigator.pop({animated: true});
};
emitAdding = (loading) => {
this.props.navigator.setButtons({
rightButtons: [{...this.addButton, disabled: loading}]
});
};
emitCanAddMembers = (enabled) => {
this.props.navigator.setButtons({
rightButtons: [{...this.addButton, disabled: !enabled}]
});
};
handleAddMembersPress = () => {
const {selectedMembers} = this.state;
const {actions, currentChannel} = this.props;
@ -159,6 +173,20 @@ class ChannelAddMembers extends PureComponent {
this.onSearchButtonPress();
};
handleRowSelect = (id) => {
const selectedMembers = Object.assign({}, this.state.selectedMembers, {[id]: !this.state.selectedMembers[id]});
if (Object.values(selectedMembers).filter((selected) => selected).length) {
this.emitCanAddMembers(true);
} else {
this.emitCanAddMembers(false);
}
this.setState({
profiles: markSelectedProfiles(this.state.profiles, selectedMembers),
selectedMembers
});
};
loadMoreMembers = () => {
const {actions, loadMoreRequestStatus, currentChannel, currentTeam} = this.props;
const {next, searching} = this.state;
@ -178,37 +206,22 @@ class ChannelAddMembers extends PureComponent {
}
};
handleRowSelect = (id) => {
const selectedMembers = Object.assign({}, this.state.selectedMembers, {[id]: !this.state.selectedMembers[id]});
if (Object.values(selectedMembers).filter((selected) => selected).length) {
this.emitCanAddMembers(true);
} else {
this.emitCanAddMembers(false);
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'add-members') {
this.handleAddMembersPress();
}
}
this.setState({
profiles: markSelectedProfiles(this.state.profiles, selectedMembers),
selectedMembers
});
};
emitCanAddMembers = (enabled) => {
EventEmitter.emit('can_add_members', enabled);
};
emitAdding = (loading) => {
this.setState({canSelect: false});
EventEmitter.emit('adding_members', loading);
};
searchBarRef = (ref) => {
this.searchBar = ref;
};
onSearchButtonPress = () => {
this.searchBar.blur();
};
searchBarRef = (ref) => {
this.searchBar = ref;
};
searchProfiles = (event) => {
const term = event.nativeEvent.text.toLowerCase();
const {actions, currentChannel, currentTeam} = this.props;
@ -225,26 +238,27 @@ class ChannelAddMembers extends PureComponent {
}
};
cancelSearch = () => {
this.setState({
searching: false,
term: null,
page: 0,
profiles: markSelectedProfiles(this.props.membersNotInChannel, this.state.selectedMembers)
});
};
render() {
const {intl, loadMoreRequestStatus, searchRequestStatus, preferences, theme} = this.props;
const {profiles, searching} = this.state;
const {adding, profiles, searching} = this.state;
const {formatMessage} = intl;
const isLoading = (loadMoreRequestStatus === RequestStatus.STARTED) ||
(searchRequestStatus === RequestStatus.STARTED);
const style = getStyleFromTheme(theme);
const more = searching ? () => true : this.loadMoreMembers;
if (adding) {
return (
<View style={style.container}>
<StatusBar barStyle='light-content'/>
<Loading/>
</View>
);
}
return (
<View style={style.container}>
<StatusBar barStyle='light-content'/>
<View
style={{marginVertical: 5}}
>

View file

@ -2,11 +2,9 @@
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import navigationSceneConnect from '../navigationSceneConnect';
import {connect} from 'react-redux';
import {handleAddChannelMembers} from 'app/actions/views/channel_add_members';
import {goBack} from 'app/actions/navigation';
import {getTheme} from 'app/selectors/preferences';
import {getCurrentChannel, getCurrentChannelStats} from 'mattermost-redux/selectors/entities/channels';
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
@ -41,11 +39,10 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
getTeamStats,
getProfilesNotInChannel,
goBack,
handleAddChannelMembers,
searchProfiles
}, dispatch)
};
}
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(ChannelAddMembers);
export default connect(mapStateToProps, mapDispatchToProps)(ChannelAddMembers);

View file

@ -6,6 +6,7 @@ import {injectIntl, intlShape} from 'react-intl';
import {
Alert,
ScrollView,
StatusBar,
StyleSheet,
View
} from 'react-native';
@ -25,6 +26,7 @@ class ChannelInfo extends PureComponent {
currentChannel: PropTypes.object.isRequired,
currentChannelCreatorName: PropTypes.string,
currentChannelMemberCount: PropTypes.number,
navigator: PropTypes.object,
status: PropTypes.string,
theme: PropTypes.object.isRequired,
isCurrent: PropTypes.bool.isRequired,
@ -35,9 +37,6 @@ class ChannelInfo extends PureComponent {
closeGMChannel: PropTypes.func.isRequired,
deleteChannel: PropTypes.func.isRequired,
getChannelStats: PropTypes.func.isRequired,
goBack: PropTypes.func.isRequired,
goToChannelAddMembers: PropTypes.func.isRequired,
goToChannelMembers: PropTypes.func.isRequired,
leaveChannel: PropTypes.func.isRequired,
markFavorite: PropTypes.func.isRequired,
unmarkFavorite: PropTypes.func.isRequired
@ -63,6 +62,41 @@ class ChannelInfo extends PureComponent {
}
}
goToChannelAddMembers = () => {
const {intl, navigator, theme} = this.props;
navigator.push({
backButtonTitle: '',
screen: 'ChannelAddMembers',
title: intl.formatMessage({id: 'channel_header.addMembers', defaultMessage: 'Add Members'}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
}
});
};
goToChannelMembers = () => {
const {canManageUsers, intl, navigator, theme} = this.props;
const id = canManageUsers ? 'channel_header.manageMembers' : 'channel_header.viewMembers';
const defaultMessage = canManageUsers ? 'Manage Members' : 'View Members';
navigator.push({
backButtonTitle: '',
screen: 'ChannelMembers',
title: intl.formatMessage({id, defaultMessage}),
animated: true,
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
}
});
};
handleDeleteOrLeave(eventType) {
const {formatMessage} = this.props.intl;
const channel = this.props.currentChannel;
@ -79,7 +113,9 @@ class ChannelInfo extends PureComponent {
defaultMessage: 'Are you sure you want to leave the {term} {name}?'
};
onPressAction = () => {
this.props.actions.leaveChannel(channel, true).then(this.props.actions.goBack);
this.props.actions.leaveChannel(channel, true).then(() => {
this.props.navigator.pop({animated: true});
});
};
} else if (eventType === 'delete') {
title = {id: 'mobile.channel_info.alertTitleDeleteChannel', defaultMessage: 'Delete {term}'};
@ -88,7 +124,9 @@ class ChannelInfo extends PureComponent {
defaultMessage: 'Are you sure you want to delete the {term} {name}?'
};
onPressAction = () => {
this.props.actions.deleteChannel(channel.id).then(this.props.actions.goBack);
this.props.actions.deleteChannel(channel.id).then(() => {
this.props.navigator.pop({animated: true});
});
};
}
@ -113,14 +151,18 @@ class ChannelInfo extends PureComponent {
handleClose = () => {
const {currentChannel, isCurrent, isFavorite} = this.props;
const channel = Object.assign({}, currentChannel, {isCurrent}, {isFavorite});
const {closeDMChannel, closeGMChannel, goBack} = this.props.actions;
const {closeDMChannel, closeGMChannel} = this.props.actions;
switch (channel.type) {
case General.DM_CHANNEL:
closeDMChannel(channel).then(goBack);
closeDMChannel(channel).then(() => {
this.props.navigator.pop({animated: true});
});
break;
case General.GM_CHANNEL:
closeGMChannel(channel).then(goBack);
closeGMChannel(channel).then(() => {
this.props.navigator.pop({animated: true});
});
break;
}
};
@ -178,6 +220,7 @@ class ChannelInfo extends PureComponent {
return (
<View style={style.container}>
<StatusBar barStyle='light-content'/>
<ScrollView
style={style.scrollView}
>
@ -217,7 +260,7 @@ class ChannelInfo extends PureComponent {
**/
}
<ChannelInfoRow
action={() => preventDoubleTap(this.props.actions.goToChannelMembers)}
action={() => preventDoubleTap(this.goToChannelMembers)}
defaultMessage={canManageUsers ? 'Manage Members' : 'View Members'}
detail={currentChannelMemberCount}
icon='users'
@ -228,7 +271,7 @@ class ChannelInfo extends PureComponent {
<View>
<View style={style.separator}/>
<ChannelInfoRow
action={() => preventDoubleTap(this.props.actions.goToChannelAddMembers)}
action={() => preventDoubleTap(this.goToChannelAddMembers)}
defaultMessage='Add Members'
icon='user-plus'
textId='channel_header.addMembers'

View file

@ -2,8 +2,8 @@
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goToChannelMembers, goToChannelAddMembers, goBack} from 'app/actions/navigation';
import {
closeDMChannel,
closeGMChannel,
@ -11,7 +11,6 @@ import {
markFavorite,
unmarkFavorite
} from 'app/actions/views/channel';
import navigationSceneConnect from 'app/scenes/navigationSceneConnect';
import {getTheme} from 'app/selectors/preferences';
import {getChannelStats, deleteChannel} from 'mattermost-redux/actions/channels';
@ -67,9 +66,6 @@ function mapDispatchToProps(dispatch) {
closeGMChannel,
deleteChannel,
getChannelStats,
goBack,
goToChannelAddMembers,
goToChannelMembers,
leaveChannel,
markFavorite,
unmarkFavorite
@ -77,4 +73,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(ChannelInfo);
export default connect(mapStateToProps, mapDispatchToProps)(ChannelInfo);

View file

@ -7,11 +7,13 @@ import {
Keyboard,
Platform,
InteractionManager,
StatusBar,
StyleSheet,
View
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import Loading from 'app/components/loading';
import MemberList from 'app/components/custom_list';
import SearchBar from 'app/components/search_bar';
import {createMembersSections, loadingText, markSelectedProfiles} from 'app/utils/member_list';
@ -19,12 +21,8 @@ import MemberListRow from 'app/components/custom_list/member_list_row';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {General, RequestStatus} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import {displayUsername, filterProfilesMatchingTerm} from 'mattermost-redux/utils/user_utils';
import ChannelMembersTitle from './channel_members_title';
import RemoveMemberButton from './remove_member_button';
class ChannelMembers extends PureComponent {
static propTypes = {
intl: intlShape.isRequired,
@ -33,28 +31,23 @@ class ChannelMembers extends PureComponent {
currentChannelMembers: PropTypes.array.isRequired,
currentChannelMemberCount: PropTypes.number.isRequired,
currentUserId: PropTypes.string.isRequired,
navigator: PropTypes.object,
preferences: PropTypes.object,
requestStatus: PropTypes.string,
searchRequestStatus: PropTypes.string,
removeMembersStatus: PropTypes.string,
canManageUsers: PropTypes.bool.isRequired,
subscribeToHeaderEvent: React.PropTypes.func,
unsubscribeFromHeaderEvent: React.PropTypes.func,
actions: PropTypes.shape({
getProfilesInChannel: PropTypes.func.isRequired,
goBack: PropTypes.func.isRequired,
handleRemoveChannelMembers: PropTypes.func.isRequired,
searchProfiles: PropTypes.func.isRequired
})
};
static navigationProps = {
renderTitleComponent: () => {
return <ChannelMembersTitle/>;
},
renderRightComponent: (props, emitter) => {
return <RemoveMemberButton emitter={emitter}/>;
}
removeButton = {
disabled: true,
id: 'remove-members',
showAsAction: 'never'
};
constructor(props) {
@ -71,10 +64,14 @@ class ChannelMembers extends PureComponent {
selectedMembers: {},
showNoResults: false
};
}
this.removeButton.title = props.intl.formatMessage({id: 'channel_members_modal.remove', defaultMessage: 'Remove'});
componentWillMount() {
this.props.subscribeToHeaderEvent('remove_members', this.handleRemoveMembersPress);
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
if (props.canManageUsers) {
props.navigator.setButtons({
rightButtons: [this.removeButton]
});
}
}
componentDidMount() {
@ -90,8 +87,6 @@ class ChannelMembers extends PureComponent {
}
componentWillUnmount() {
this.props.unsubscribeFromHeaderEvent('remove_members');
if (Platform.OS === 'android') {
Keyboard.removeListener('keyboardDidHide', this.handleAndroidKeyboard);
}
@ -122,20 +117,51 @@ class ChannelMembers extends PureComponent {
switch (removeMembersStatus) {
case RequestStatus.STARTED:
this.emitRemoving(true);
this.setState({error: null});
this.setState({error: null, canSelect: false, removing: true});
break;
case RequestStatus.SUCCESS:
this.emitRemoving(false);
this.setState({error: null});
this.props.actions.goBack();
this.setState({error: null, canSelect: false, removing: false});
this.close();
break;
case RequestStatus.FAILURE:
this.emitRemoving(false);
this.setState({canSelect: true, removing: false});
break;
}
}
}
cancelSearch = () => {
this.setState({
searching: false,
term: null,
page: 0,
profiles: markSelectedProfiles(this.props.currentChannelMembers, this.state.selectedMembers)
});
};
close = () => {
this.props.navigator.pop({animated: true});
};
emitCanRemoveMembers = (enabled) => {
this.props.navigator.setButtons({
rightButtons: [{...this.removeButton, disabled: !enabled}]
});
};
emitRemoving = (loading) => {
this.setState({canSelect: false, removing: loading});
this.props.navigator.setButtons({
rightButtons: [{...this.removeButton, disabled: loading}]
});
};
handleAndroidKeyboard = () => {
this.onSearchButtonPress();
};
handleRemoveMembersPress = () => {
const {selectedMembers} = this.state;
const membersToRemove = Object.keys(selectedMembers).filter((m) => selectedMembers[m]);
@ -173,13 +199,17 @@ class ChannelMembers extends PureComponent {
);
};
handleAndroidKeyboard = () => {
this.onSearchButtonPress();
};
removeMembers = (membersToRemove) => {
const {actions, currentChannel} = this.props;
actions.handleRemoveChannelMembers(currentChannel.id, membersToRemove);
handleRowSelect = (id) => {
const selectedMembers = Object.assign({}, this.state.selectedMembers, {[id]: !this.state.selectedMembers[id]});
if (Object.values(selectedMembers).filter((selected) => selected).length) {
this.emitCanRemoveMembers(true);
} else {
this.emitCanRemoveMembers(false);
}
this.setState({
profiles: markSelectedProfiles(this.state.profiles, selectedMembers),
selectedMembers
});
};
loadMoreMembers = () => {
@ -201,58 +231,21 @@ class ChannelMembers extends PureComponent {
}
};
handleRowSelect = (id) => {
const selectedMembers = Object.assign({}, this.state.selectedMembers, {[id]: !this.state.selectedMembers[id]});
if (Object.values(selectedMembers).filter((selected) => selected).length) {
this.emitCanRemoveMembers(true);
} else {
this.emitCanRemoveMembers(false);
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'remove-members') {
this.handleRemoveMembersPress();
}
}
this.setState({
profiles: markSelectedProfiles(this.state.profiles, selectedMembers),
selectedMembers
});
};
emitCanRemoveMembers = (enabled) => {
EventEmitter.emit('can_remove_members', enabled);
};
emitRemoving = (loading) => {
this.setState({canSelect: false});
EventEmitter.emit('removing_members', loading);
};
searchBarRef = (ref) => {
this.searchBar = ref;
};
onSearchButtonPress = () => {
this.searchBar.blur();
};
searchProfiles = (event) => {
const term = event.nativeEvent.text.toLowerCase();
if (term) {
this.setState({searching: true, term});
clearTimeout(this.searchTimeoutId);
this.searchTimeoutId = setTimeout(() => {
this.props.actions.searchProfiles(term, {in_channel_id: this.props.currentChannel.id});
}, General.SEARCH_TIMEOUT_MILLISECONDS);
} else {
this.cancelSearch();
}
};
cancelSearch = () => {
this.setState({
searching: false,
term: null,
page: 0,
profiles: markSelectedProfiles(this.props.currentChannelMembers, this.state.selectedMembers)
});
removeMembers = (membersToRemove) => {
const {actions, currentChannel} = this.props;
actions.handleRemoveChannelMembers(currentChannel.id, membersToRemove);
};
renderMemberRow = (user, sectionId, rowId, preferences, theme, selectable, onPress, onSelect) => {
@ -281,17 +274,46 @@ class ChannelMembers extends PureComponent {
);
};
searchBarRef = (ref) => {
this.searchBar = ref;
};
searchProfiles = (event) => {
const term = event.nativeEvent.text.toLowerCase();
if (term) {
this.setState({searching: true, term});
clearTimeout(this.searchTimeoutId);
this.searchTimeoutId = setTimeout(() => {
this.props.actions.searchProfiles(term, {in_channel_id: this.props.currentChannel.id});
}, General.SEARCH_TIMEOUT_MILLISECONDS);
} else {
this.cancelSearch();
}
};
render() {
const {canManageUsers, intl, preferences, requestStatus, searchRequestStatus, theme} = this.props;
const {formatMessage} = intl;
const {profiles, searching, showNoResults} = this.state;
const {profiles, removing, searching, showNoResults} = this.state;
const isLoading = (requestStatus === RequestStatus.STARTED) || (requestStatus.status === RequestStatus.NOT_STARTED) ||
(searchRequestStatus === RequestStatus.STARTED);
const more = searching ? () => true : this.loadMoreMembers;
const style = getStyleFromTheme(theme);
if (removing) {
return (
<View style={style.container}>
<StatusBar barStyle='light-content'/>
<Loading/>
</View>
);
}
return (
<View style={style.container}>
<StatusBar barStyle='light-content'/>
<View
style={{marginVertical: 5}}
>

View file

@ -2,10 +2,8 @@
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import navigationSceneConnect from '../navigationSceneConnect';
import {goBack} from 'app/actions/navigation';
import {handleRemoveChannelMembers} from 'app/actions/views/channel_members';
import {getTheme} from 'app/selectors/preferences';
import {getCurrentChannel, getCurrentChannelStats, canManageChannelMembers} from 'mattermost-redux/selectors/entities/channels';
@ -36,11 +34,10 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getProfilesInChannel,
goBack,
handleRemoveChannelMembers,
searchProfiles
}, dispatch)
};
}
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(ChannelMembers);
export default connect(mapStateToProps, mapDispatchToProps)(ChannelMembers);

View file

@ -0,0 +1,410 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {
Dimensions,
Keyboard,
InteractionManager,
Platform,
StatusBar,
StyleSheet,
TouchableWithoutFeedback,
View,
findNodeHandle
} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import {General, RequestStatus} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import Loading from 'app/components/loading';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
class CreateChannel extends PureComponent {
static propTypes = {
intl: intlShape.isRequired,
createChannelRequest: PropTypes.object.isRequired,
navigator: PropTypes.object,
theme: PropTypes.object.isRequired,
channelType: PropTypes.string,
closeButton: PropTypes.object,
actions: PropTypes.shape({
handleCreateChannel: PropTypes.func.isRequired
})
};
static defaultProps = {
channelType: General.OPEN_CHANNEL
};
leftButton = {
id: 'close-new-channel'
};
rightButton = {
id: 'create-channel',
disabled: true,
showAsAction: 'never'
};
constructor(props) {
super(props);
this.state = {
displayName: '',
header: '',
purpose: ''
};
this.rightButton.title = props.intl.formatMessage({id: 'mobile.create_channel', defaultMessage: 'Create'});
if (props.channelType === General.PRIVATE_CHANNEL) {
this.left = {...this.leftButton, icon: props.closeButton};
}
const buttons = {
rightButtons: [this.rightButton]
};
if (this.left) {
buttons.leftButtons = [this.left];
}
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
props.navigator.setButtons(buttons);
}
componentDidMount() {
this.emitCanCreateChannel(false);
}
componentWillReceiveProps(nextProps) {
const {createChannelRequest} = nextProps;
if (this.props.createChannelRequest !== createChannelRequest) {
switch (createChannelRequest.status) {
case RequestStatus.STARTED:
this.emitCreating(true);
this.setState({error: null, creating: true});
break;
case RequestStatus.SUCCESS:
EventEmitter.emit('close_channel_drawer');
InteractionManager.runAfterInteractions(() => {
this.emitCreating(false);
this.setState({error: null, creating: false});
this.close(false);
});
break;
case RequestStatus.FAILURE:
this.emitCreating(false);
this.setState({error: createChannelRequest.error, creating: false});
break;
}
}
}
blur = () => {
this.nameInput.refs.wrappedInstance.blur();
this.purposeInput.refs.wrappedInstance.blur();
this.headerInput.refs.wrappedInstance.blur();
this.scroll.scrollToPosition(0, 0, true);
};
channelNameRef = (ref) => {
this.nameInput = ref;
};
channelPurposeRef = (ref) => {
this.purposeInput = ref;
};
channelHeaderRef = (ref) => {
this.headerInput = ref;
};
close = (goBack = false) => {
if (goBack) {
this.props.navigator.pop({animated: true});
} else {
this.props.navigator.dismissAllModals({
animationType: 'slide-down'
});
}
};
emitCanCreateChannel = (enabled) => {
const buttons = {
rightButtons: [{...this.rightButton, disabled: !enabled}]
};
if (this.left) {
buttons.leftButtons = [this.left];
}
this.props.navigator.setButtons(buttons);
};
emitCreating = (loading) => {
const buttons = {
rightButtons: [{...this.rightButton, disabled: loading}]
};
if (this.left) {
buttons.leftButtons = [this.left];
}
this.props.navigator.setButtons(buttons);
};
lastTextRef = (ref) => {
this.lastText = ref;
};
onCreateChannel = () => {
Keyboard.dismiss();
const {displayName, purpose, header} = this.state;
this.props.actions.handleCreateChannel(displayName, purpose, header, this.props.channelType);
};
onDisplayNameChangeText = (displayName) => {
this.setState({displayName});
if (displayName && displayName.length >= 2) {
this.emitCanCreateChannel(true);
} else {
this.emitCanCreateChannel(false);
}
};
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
switch (event.id) {
case 'close-new-channel':
this.close(this.props.channelType === General.OPEN_CHANNEL);
break;
case 'create-channel':
this.onCreateChannel();
break;
}
}
};
onPurposeChangeText = (purpose) => {
this.setState({purpose});
};
onHeaderChangeText = (header) => {
this.setState({header});
};
scrollRef = (ref) => {
this.scroll = ref;
};
scrollToEnd = () => {
this.scroll.scrollToFocusedInput(findNodeHandle(this.lastText));
};
render() {
const {theme} = this.props;
const {creating, displayName, header, purpose, error} = this.state;
const {height, width} = Dimensions.get('window');
const style = getStyleSheet(theme);
if (creating) {
return (
<View style={{flex: 1}}>
<StatusBar barStyle='light-content'/>
<Loading/>
</View>
);
}
let displayError;
if (error) {
displayError = (
<View style={[style.errorContainer, {width}]}>
<View style={style.errorWrapper}>
<ErrorText error={error}/>
</View>
</View>
);
}
return (
<View style={{flex: 1}}>
<StatusBar barStyle='light-content'/>
<KeyboardAwareScrollView
ref={this.scrollRef}
style={style.container}
>
<TouchableWithoutFeedback onPress={this.blur}>
<View style={[style.scrollView, {height: height + (Platform.OS === 'android' ? 200 : 0)}]}>
{displayError}
<View>
<FormattedText
style={[style.title, {marginTop: (error ? 10 : 0)}]}
id='channel_modal.name'
defaultMessage='Name'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelNameRef}
value={displayName}
onChangeText={this.onDisplayNameChangeText}
style={style.input}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'channel_modal.nameEx', defaultMessage: 'E.g.: "Bugs", "Marketing", "客户支持"'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
underlineColorAndroid='transparent'
/>
</View>
<View style={style.titleContainer30}>
<FormattedText
style={style.title}
id='channel_modal.purpose'
defaultMessage='Purpose'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelPurposeRef}
value={purpose}
onChangeText={this.onPurposeChangeText}
style={[style.input, {height: 110}]}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'channel_modal.purposeEx', defaultMessage: 'E.g.: "A channel to file bugs and improvements"'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
multiline={true}
underlineColorAndroid='transparent'
/>
</View>
<View>
<FormattedText
style={style.helpText}
id='channel_modal.descriptionHelp'
defaultMessage='Describe how this channel should be used.'
/>
</View>
<View style={style.titleContainer15}>
<FormattedText
style={style.title}
id='channel_modal.header'
defaultMessage='Header'
/>
<FormattedText
style={style.optional}
id='channel_modal.optional'
defaultMessage='(optional)'
/>
</View>
<View style={style.inputContainer}>
<TextInputWithLocalizedPlaceholder
ref={this.channelHeaderRef}
value={header}
onChangeText={this.onHeaderChangeText}
style={[style.input, {height: 110}]}
autoCapitalize='none'
autoCorrect={false}
placeholder={{id: 'channel_modal.headerEx', defaultMessage: 'E.g.: "[Link Title](http://example.com)"'}}
placeholderTextColor={changeOpacity('#000', 0.5)}
multiline={true}
onFocus={this.scrollToEnd}
underlineColorAndroid='transparent'
/>
</View>
<View ref={this.lastTextRef}>
<FormattedText
style={style.helpText}
id='channel_modal.headerHelp'
defaultMessage={'Set text that will appear in the header of the channel beside the channel name. For example, include frequently used links by typing [Link Title](http://example.com).'}
/>
</View>
</View>
</TouchableWithoutFeedback>
</KeyboardAwareScrollView>
</View>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.centerChannelBg
},
scrollView: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
paddingTop: 30
},
errorContainer: {
position: 'absolute'
},
errorWrapper: {
justifyContent: 'center',
alignItems: 'center',
marginBottom: 10
},
inputContainer: {
marginTop: 10,
backgroundColor: '#fff'
},
input: {
color: '#333',
fontSize: 14,
height: 40,
paddingHorizontal: 15
},
titleContainer30: {
flexDirection: 'row',
marginTop: 30
},
titleContainer15: {
flexDirection: 'row',
marginTop: 15
},
title: {
fontSize: 14,
color: theme.centerChannelColor,
marginLeft: 15
},
optional: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 14,
marginLeft: 5
},
helpText: {
fontSize: 14,
color: changeOpacity(theme.centerChannelColor, 0.5),
marginTop: 10,
marginHorizontal: 15
},
navTitle: {
...Platform.select({
android: {
fontSize: 18
},
ios: {
fontSize: 15,
fontWeight: 'bold'
}
})
}
});
});
export default injectIntl(CreateChannel);

View file

@ -2,10 +2,8 @@
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import navigationSceneConnect from '../navigationSceneConnect';
import {goBack, closeModal} from 'app/actions/navigation';
import {handleCreateChannel} from 'app/actions/views/create_channel';
import {getTheme} from 'app/selectors/preferences';
@ -26,11 +24,9 @@ function mapStateToProps(state, ownProps) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goBack,
closeModal,
handleCreateChannel
}, dispatch)
};
}
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(CreateChannel);
export default connect(mapStateToProps, mapDispatchToProps)(CreateChannel);

View file

@ -1,106 +1,60 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {
Dimensions,
Platform,
StatusBar,
StyleSheet,
TouchableOpacity,
View
} from 'react-native';
import ActionButton from 'app/components/action_button';
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import Loading from 'app/components/loading';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {RequestStatus} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
export default class EditPost extends PureComponent {
class EditPost extends PureComponent {
static propTypes = {
editPostRequest: PropTypes.object.isRequired,
post: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
subscribeToHeaderEvent: React.PropTypes.func.isRequired,
unsubscribeFromHeaderEvent: React.PropTypes.func.isRequired,
actions: PropTypes.shape({
closeModal: PropTypes.func.isRequired,
editPost: PropTypes.func.isRequired
})
}),
closeButton: PropTypes.object,
editPostRequest: PropTypes.object.isRequired,
intl: intlShape.isRequired,
navigator: PropTypes.object,
post: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
static navigationProps = {
renderLeftComponent: (props, emitter, theme) => {
return (
<TouchableOpacity
style={{flex: 1, paddingHorizontal: 15, justifyContent: 'center'}}
onPress={() => emitter('close')}
>
<FormattedText
id='edit_post.cancel'
defaultMessage='Cancel'
style={{color: theme.sidebarHeaderTextColor}}
/>
</TouchableOpacity>
);
},
renderRightComponent: (props, emitter) => {
return (
<ActionButton
actionEventName='edit_post'
emitter={emitter}
enabled={true}
enableEventName='can_edit_post'
labelDefaultMessage='Save'
labelId='edit_post.save'
loadingEventName='editing_post'
/>
);
}
leftButton = {
id: 'close-edit-post'
};
rightButton = {
id: 'edit-post',
showAsAction: 'always'
};
constructor(props) {
super(props);
this.state = {message: props.post.message};
this.rightButton.title = props.intl.formatMessage({id: 'edit_post.save', defaultMessage: 'Save'});
props.navigator.setOnNavigatorEvent(this.onNavigatorEvent);
props.navigator.setButtons({
leftButtons: [{...this.leftButton, icon: props.closeButton}],
rightButtons: [this.rightButton]
});
}
onEditPost = async () => {
const {message} = this.state;
const post = Object.assign({}, this.props.post, {message});
await this.props.actions.editPost(post);
};
onPostChangeText = (message) => {
this.setState({message});
if (message) {
this.emitCanEditPost(true);
} else {
this.emitCanEditPost(false);
}
};
emitCanEditPost = (enabled) => {
EventEmitter.emit('can_edit_post', enabled);
};
emitEditing = (loading) => {
EventEmitter.emit('editing_post', loading);
};
focus = () => {
this.messageInput.refs.wrappedInstance.focus();
};
messageRef = (ref) => {
this.messageInput = ref;
};
componentWillMount() {
this.props.subscribeToHeaderEvent('close', this.props.actions.closeModal);
this.props.subscribeToHeaderEvent('edit_post', this.onEditPost);
componentDidMount() {
this.focus();
}
componentWillReceiveProps(nextProps) {
@ -110,29 +64,44 @@ export default class EditPost extends PureComponent {
switch (editPostRequest.status) {
case RequestStatus.STARTED:
this.emitEditing(true);
this.setState({error: null});
this.setState({error: null, editing: true});
break;
case RequestStatus.SUCCESS:
this.emitEditing(false);
this.setState({error: null});
this.props.actions.closeModal();
this.setState({error: null, editing: false});
this.close();
break;
case RequestStatus.FAILURE:
this.emitEditing(false);
this.setState({error: editPostRequest.error});
this.setState({error: editPostRequest.error, editing: false});
break;
}
}
}
componentDidMount() {
this.focus();
}
close = () => {
this.props.navigator.dismissModal({
animationType: 'slide-down'
});
};
componentWillUnmount() {
this.props.unsubscribeFromHeaderEvent('close');
this.props.unsubscribeFromHeaderEvent('edit_post');
}
emitCanEditPost = (enabled) => {
this.props.navigator.setButtons({
leftButtons: [this.leftButton],
rightButtons: [{...this.rightButton, disabled: !enabled}]
});
};
emitEditing = (loading) => {
this.props.navigator.setButtons({
leftButtons: [this.leftButton],
rightButtons: [{...this.rightButton, disabled: loading}]
});
};
focus = () => {
this.messageInput.refs.wrappedInstance.focus();
};
handleSubmit = () => {
// Workaround for android as the multiline is not working
@ -148,13 +117,54 @@ export default class EditPost extends PureComponent {
}
};
messageRef = (ref) => {
this.messageInput = ref;
};
onEditPost = () => {
const {message} = this.state;
const post = Object.assign({}, this.props.post, {message});
this.props.actions.editPost(post);
};
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
switch (event.id) {
case 'close-edit-post':
this.close();
break;
case 'edit-post':
this.onEditPost();
break;
}
}
};
onPostChangeText = (message) => {
this.setState({message});
if (message) {
this.emitCanEditPost(true);
} else {
this.emitCanEditPost(false);
}
};
render() {
const {theme} = this.props;
const {message, error} = this.state;
const {editing, message, error} = this.state;
const {height, width} = Dimensions.get('window');
const style = getStyleSheet(theme);
if (editing) {
return (
<View style={style.container}>
<StatusBar barStyle='light-content'/>
<Loading/>
</View>
);
}
let displayError;
if (error) {
displayError = (
@ -172,6 +182,7 @@ export default class EditPost extends PureComponent {
style={style.container}
keyboardVerticalOffset={0}
>
<StatusBar barStyle='light-content'/>
<View style={style.scrollView}>
{displayError}
<View style={[style.inputContainer, {height: Platform.OS === 'android' ? (height / 2) - 20 : (height / 2)}]}>
@ -231,3 +242,5 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
}
});
});
export default injectIntl(EditPost);

View file

@ -2,10 +2,8 @@
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import navigationSceneConnect from '../navigationSceneConnect';
import {closeModal} from 'app/actions/navigation';
import {getTheme} from 'app/selectors/preferences';
import {editPost} from 'mattermost-redux/actions/posts';
@ -26,10 +24,9 @@ function mapStateToProps(state, ownProps) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
closeModal,
editPost
}, dispatch)
};
}
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(EditPost);
export default connect(mapStateToProps, mapDispatchToProps)(EditPost);

View file

@ -37,21 +37,15 @@ const STATUSBAR_HEIGHT = Platform.select({
export default class ImagePreview extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
addFileToFetchCache: PropTypes.func.isRequired,
goBack: PropTypes.func.isRequired
addFileToFetchCache: PropTypes.func.isRequired
}),
fetchCache: PropTypes.object.isRequired,
fileId: PropTypes.string.isRequired,
files: PropTypes.array.isRequired,
navigator: PropTypes.object,
theme: PropTypes.object.isRequired
};
static navigationProps = {
allowSceneSwipe: false,
hideNavBar: true,
modalAnimationType: 'fade'
};
constructor(props) {
super(props);
@ -111,6 +105,10 @@ export default class ImagePreview extends PureComponent {
}
}
close = () => {
this.props.navigator.dismissModal({animationType: 'none'});
};
mainViewMoveShouldSetPanResponderCapture = (evt, gestureState) => {
if (gestureState.numberActiveTouches === 2 || this.state.isZooming) {
return false;
@ -124,24 +122,24 @@ export default class ImagePreview extends PureComponent {
}
return false;
}
};
mainViewPanResponderRelease = (evt, gestureState) => {
if (Math.abs(gestureState.dy) > DRAG_VERTICAL_THRESHOLD_END) {
this.props.actions.goBack();
this.close();
} else {
this.setHeaderAndFileInfoVisible(true);
Animated.spring(this.state.drag, {
toValue: {x: 0, y: 0}
}).start();
}
}
};
handleClose = () => {
if (this.state.showFileInfo) {
this.props.actions.goBack();
this.close();
}
}
};
handleImageTap = () => {
/*if (!this.lastPress) {
@ -157,7 +155,7 @@ export default class ImagePreview extends PureComponent {
}*/
this.setHeaderAndFileInfoVisible(!this.state.showFileInfo);
}
};
setHeaderAndFileInfoVisible = (show) => {
this.setState({
@ -174,7 +172,7 @@ export default class ImagePreview extends PureComponent {
toValue: opacity,
duration: 300
}).start();
}
};
handleScroll = (event) => {
if (event.nativeEvent.contentOffset.x % this.state.deviceWidth === 0) {
@ -183,11 +181,11 @@ export default class ImagePreview extends PureComponent {
pagingEnabled: true
});
}
}
};
attachScrollView = (c) => {
this.scrollView = c;
}
};
onLayout = (event) => {
if (event.nativeEvent.layout.width !== this.state.deviceWidth) {
@ -196,7 +194,7 @@ export default class ImagePreview extends PureComponent {
deviceWidth: event.nativeEvent.layout.width
});
}
}
};
imageIsZooming = (zooming) => {
if (zooming !== this.state.isZooming) {
@ -205,7 +203,7 @@ export default class ImagePreview extends PureComponent {
isZooming: zooming
});
}
}
};
render() {
const maxImageHeight = this.state.deviceHeight - STATUSBAR_HEIGHT;

View file

@ -2,16 +2,14 @@
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goBack} from 'app/actions/navigation';
import {addFileToFetchCache} from 'app/actions/views/file_preview';
import {getTheme} from 'app/selectors/preferences';
import {makeGetFilesForPost} from 'mattermost-redux/selectors/entities/files';
import ImagePreview from './image_preview';
import navigationSceneConnect from '../navigationSceneConnect';
function makeMapStateToProps() {
const getFilesForPost = makeGetFilesForPost();
return function mapStateToProps(state, ownProps) {
@ -27,10 +25,9 @@ function makeMapStateToProps() {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
addFileToFetchCache,
goBack
addFileToFetchCache
}, dispatch)
};
}
export default navigationSceneConnect(makeMapStateToProps, mapDispatchToProps)(ImagePreview);
export default connect(makeMapStateToProps, mapDispatchToProps)(ImagePreview);

72
app/screens/index.js Normal file
View file

@ -0,0 +1,72 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {Navigation} from 'react-native-navigation';
import About from 'app/screens/about';
import AccountSettings from 'app/screens/account_settings';
import AccountNotifications from 'app/screens/account_notifications';
import Channel from 'app/screens/channel';
import ChannelAddMembers from 'app/screens/channel_add_members';
import ChannelInfo from 'app/screens/channel_info';
import ChannelMembers from 'app/screens/channel_members';
import CreateChannel from 'app/screens/create_channel';
import EditPost from 'app/screens/edit_post';
import ImagePreview from 'app/screens/image_preview';
import LoadTeam from 'app/screens/load_team';
import Login from 'app/screens/login';
import LoginOptions from 'app/screens/login_options';
import Mfa from 'app/screens/mfa';
import MoreChannels from 'app/screens/more_channels';
import MoreDirectMessages from 'app/screens/more_dms';
import Notification from 'app/screens/notification';
import OptionsModal from 'app/screens/options_modal';
import Root from 'app/screens/root';
import Saml from 'app/screens/saml';
import SelectServer from 'app/screens/select_server';
import SelectTeam from 'app/screens/select_team';
import Settings from 'app/screens/settings';
import Thread from 'app/screens/thread';
import UserProfile from 'app/screens/user_profile';
import IntlWrapper from 'app/components/root';
function wrapWithContextProvider(Comp) {
return (props) => { //eslint-disable-line react/display-name
const {navigator} = props; //eslint-disable-line react/prop-types
return (
<IntlWrapper navigator={navigator}>
<Comp {...props}/>
</IntlWrapper>
);
};
}
export function registerScreens(store, Provider) {
Navigation.registerComponent('About', () => wrapWithContextProvider(About), store, Provider);
Navigation.registerComponent('AccountSettings', () => wrapWithContextProvider(AccountSettings), store, Provider);
Navigation.registerComponent('AccountNotifications', () => wrapWithContextProvider(AccountNotifications), store, Provider);
Navigation.registerComponent('Channel', () => wrapWithContextProvider(Channel), store, Provider);
Navigation.registerComponent('ChannelAddMembers', () => wrapWithContextProvider(ChannelAddMembers), store, Provider);
Navigation.registerComponent('ChannelInfo', () => wrapWithContextProvider(ChannelInfo), store, Provider);
Navigation.registerComponent('ChannelMembers', () => wrapWithContextProvider(ChannelMembers), store, Provider);
Navigation.registerComponent('CreateChannel', () => wrapWithContextProvider(CreateChannel), store, Provider);
Navigation.registerComponent('EditPost', () => wrapWithContextProvider(EditPost), store, Provider);
Navigation.registerComponent('ImagePreview', () => wrapWithContextProvider(ImagePreview), store, Provider);
Navigation.registerComponent('LoadTeam', () => LoadTeam, store, Provider);
Navigation.registerComponent('Login', () => wrapWithContextProvider(Login), store, Provider);
Navigation.registerComponent('LoginOptions', () => wrapWithContextProvider(LoginOptions), store, Provider);
Navigation.registerComponent('MFA', () => wrapWithContextProvider(Mfa), store, Provider);
Navigation.registerComponent('MoreChannels', () => wrapWithContextProvider(MoreChannels), store, Provider);
Navigation.registerComponent('MoreDirectMessages', () => wrapWithContextProvider(MoreDirectMessages), store, Provider);
Navigation.registerComponent('OptionsModal', () => wrapWithContextProvider(OptionsModal), store, Provider);
Navigation.registerComponent('Notification', () => Notification, store, Provider);
Navigation.registerComponent('Root', () => Root, store, Provider);
Navigation.registerComponent('SAML', () => wrapWithContextProvider(Saml), store, Provider);
Navigation.registerComponent('SelectServer', () => wrapWithContextProvider(SelectServer), store, Provider);
Navigation.registerComponent('SelectTeam', () => wrapWithContextProvider(SelectTeam), store, Provider);
Navigation.registerComponent('Settings', () => wrapWithContextProvider(Settings), store, Provider);
Navigation.registerComponent('Thread', () => wrapWithContextProvider(Thread), store, Provider);
Navigation.registerComponent('UserProfile', () => wrapWithContextProvider(UserProfile), store, Provider);
}

View file

@ -2,20 +2,20 @@
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {goToChannelView} from 'app/actions/views/load_team';
import {initialize} from 'app/actions/views/load_team';
import {clearNotification, goToNotification} from 'app/actions/views/root';
import {handleTeamChange} from 'app/actions/views/select_team';
import {getTheme} from 'app/selectors/preferences';
import navigationSceneConnect from '../navigationSceneConnect';
import LoadTeam from './load_team';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
return {
...ownProps,
config: state.entities.general.config,
theme: getTheme(state),
teamsRequest: state.requests.teams.getMyTeams,
@ -30,11 +30,11 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
clearNotification,
goToChannelView,
goToNotification,
handleTeamChange
handleTeamChange,
initialize
}, dispatch)
};
}
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(LoadTeam);
export default connect(mapStateToProps, mapDispatchToProps)(LoadTeam);

View file

@ -2,7 +2,7 @@
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {View} from 'react-native';
import {StatusBar, View} from 'react-native';
import {RequestStatus} from 'mattermost-redux/constants';
@ -10,6 +10,7 @@ import ChannelLoader from 'app/components/channel_loader';
export default class LoadTeam extends PureComponent {
static propTypes = {
navigator: PropTypes.object,
notification: PropTypes.object,
teams: PropTypes.object.isRequired,
myMembers: PropTypes.object.isRequired,
@ -17,26 +18,25 @@ export default class LoadTeam extends PureComponent {
currentTeam: PropTypes.object,
actions: PropTypes.shape({
clearNotification: PropTypes.func.isRequired,
goToChannelView: PropTypes.func.isRequired,
goToNotification: PropTypes.func.isRequired,
handleTeamChange: PropTypes.func.isRequired
handleTeamChange: PropTypes.func.isRequired,
initialize: PropTypes.func.isRequired
}).isRequired,
theme: PropTypes.object.isRequired
};
static navigationProps = {
renderBackButton: () => null,
renderTitleComponent: () => null
};
componentDidMount() {
const {notification, currentTeam, myMembers, teams} = this.props;
const {clearNotification, goToNotification} = this.props.actions;
const {currentTeam, myMembers, teams} = this.props;
if (notification) {
clearNotification();
goToNotification(notification);
} else if (currentTeam) {
// TODO: GO TO CHANNEL WHEN NOTIFICATION IS TAPPED AND THE APP IS CLOSED
// const {notification, currentTeam, myMembers, teams} = this.props;
// const {clearNotification, goToNotification} = this.props.actions;
// if (notification) {
// clearNotification();
// goToNotification(notification);
// this.goToChannelView();
if (currentTeam) {
this.onSelectTeam(currentTeam);
} else if (!currentTeam) {
this.selectFirstTeam(teams, myMembers);
@ -60,13 +60,30 @@ export default class LoadTeam extends PureComponent {
}
onSelectTeam(team) {
const {handleTeamChange, goToChannelView} = this.props.actions;
handleTeamChange(team).then(goToChannelView);
const {handleTeamChange} = this.props.actions;
handleTeamChange(team).then(this.goToChannelView);
}
goToChannelView = () => {
const {actions, navigator, theme} = this.props;
actions.initialize();
navigator.resetTo({
screen: 'Channel',
animated: false,
navigatorStyle: {
navBarHidden: true,
statusBarHidden: false,
statusBarHideWithNavBar: false,
screenBackgroundColor: theme.centerChannelBg
}
});
};
render() {
return (
<View style={{flex: 1}}>
<StatusBar barStyle='light-content'/>
<ChannelLoader theme={this.props.theme}/>
</View>
);

View file

@ -2,24 +2,26 @@
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import navigationSceneConnect from '../navigationSceneConnect';
import {connect} from 'react-redux';
import LoginActions from 'app/actions/views/login';
import {goToMfa, goToLoadTeam} from 'app/actions/navigation';
import {getTheme} from 'app/selectors/preferences';
import {checkMfa, login} from 'mattermost-redux/actions/users';
import Login from './login.js';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {checkMfa: checkMfaRequest, login: loginRequest} = state.requests.users;
const {config, license} = state.entities.general;
return {
...ownProps,
...state.views.login,
checkMfaRequest,
loginRequest,
config,
license
license,
theme: getTheme(state)
};
}
@ -28,11 +30,9 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
...LoginActions,
checkMfa,
login,
goToMfa,
goToLoadTeam
login
}, dispatch)
};
}
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(Login);
export default connect(mapStateToProps, mapDispatchToProps)(Login);

View file

@ -1,14 +1,16 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component, PropTypes} from 'react';
import React, {PureComponent, PropTypes} from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {
ActivityIndicator,
Image,
InteractionManager,
Keyboard,
KeyboardAvoidingView,
Platform,
StatusBar,
Text,
TextInput,
TouchableWithoutFeedback,
@ -24,17 +26,17 @@ import logo from 'assets/images/logo.png';
import {RequestStatus} from 'mattermost-redux/constants';
class Login extends Component {
class Login extends PureComponent {
static propTypes = {
intl: intlShape.isRequired,
navigator: PropTypes.object,
theme: PropTypes.object,
actions: React.PropTypes.shape({
handleLoginIdChanged: React.PropTypes.func.isRequired,
handlePasswordChanged: React.PropTypes.func.isRequired,
handleSuccessfulLogin: React.PropTypes.func.isRequired,
checkMfa: React.PropTypes.func.isRequired,
login: React.PropTypes.func.isRequired,
goToMfa: React.PropTypes.func.isRequired,
goToLoadTeam: React.PropTypes.func.isRequired
login: React.PropTypes.func.isRequired
}).isRequired,
config: PropTypes.object.isRequired,
license: PropTypes.object.isRequired,
@ -60,7 +62,7 @@ class Login extends Component {
componentWillReceiveProps(nextProps) {
if (this.props.loginRequest.status === RequestStatus.STARTED && nextProps.loginRequest.status === RequestStatus.SUCCESS) {
this.props.actions.handleSuccessfulLogin().then(this.props.actions.goToLoadTeam);
this.props.actions.handleSuccessfulLogin().then(this.goToLoadTeam);
}
}
@ -70,6 +72,40 @@ class Login extends Component {
}
}
goToLoadTeam = () => {
const {navigator, theme} = this.props;
navigator.resetTo({
screen: 'LoadTeam',
title: '',
animated: false,
backButtonTitle: '',
navigatorStyle: {
statusBarHidden: false,
statusBarHideWithNavBar: false,
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
}
});
};
goToMfa = () => {
const {intl, navigator, theme} = this.props;
navigator.push({
screen: 'MFA',
title: intl.formatMessage({id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'}),
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
}
});
};
handleAndroidKeyboard = () => {
this.blur();
};
@ -82,56 +118,62 @@ class Login extends Component {
preSignIn = () => {
this.setState({error: null});
Keyboard.dismiss();
if (!this.props.loginId) {
// it's slightly weird to be constructing the message ID, but it's a bit nicer than triply nested if statements
let msgId = 'login.no';
if (this.props.config.EnableSignInWithEmail === 'true') {
msgId += 'Email';
}
if (this.props.config.EnableSignInWithUsername === 'true') {
msgId += 'Username';
}
if (this.props.license.IsLicensed === 'true' && this.props.config.EnableLdap === 'true') {
msgId += 'LdapUsername';
}
InteractionManager.runAfterInteractions(() => {
if (!this.props.loginId) {
// it's slightly weird to be constructing the message ID, but it's a bit nicer than triply nested if statements
let msgId = 'login.no';
if (this.props.config.EnableSignInWithEmail === 'true') {
msgId += 'Email';
}
if (this.props.config.EnableSignInWithUsername === 'true') {
msgId += 'Username';
}
if (this.props.license.IsLicensed === 'true' && this.props.config.EnableLdap === 'true') {
msgId += 'LdapUsername';
}
this.setState({
error: {
intl: {
id: msgId,
defaultMessage: '',
values: {
ldapUsername: this.props.config.LdapLoginFieldName ||
this.props.intl.formatMessage({id: 'login.ldapUsernameLower', defaultMessage: 'AD/LDAP username'})
this.setState({
error: {
intl: {
id: msgId,
defaultMessage: '',
values: {
ldapUsername: this.props.config.LdapLoginFieldName ||
this.props.intl.formatMessage({
id: 'login.ldapUsernameLower',
defaultMessage: 'AD/LDAP username'
})
}
}
}
}
});
return;
}
});
return;
}
if (!this.props.password) {
this.setState({
error: {
intl: {
id: 'login.noPassword',
defaultMessage: 'Please enter your password'
if (!this.props.password) {
this.setState({
error: {
intl: {
id: 'login.noPassword',
defaultMessage: 'Please enter your password'
}
}
}
});
return;
}
if (this.props.config.EnableMultifactorAuthentication === 'true') {
this.props.actions.checkMfa(this.props.loginId).then((result) => {
if (result) {
this.props.actions.goToMfa();
} else {
this.signIn();
}
});
} else {
this.signIn();
}
});
return;
}
if (this.props.config.EnableMultifactorAuthentication === 'true') {
this.props.actions.checkMfa(this.props.loginId).then((result) => {
if (result) {
this.goToMfa();
} else {
this.signIn();
}
});
} else {
this.signIn();
}
});
};
signIn = () => {
@ -256,8 +298,9 @@ class Login extends Component {
<KeyboardAvoidingView
behavior='padding'
style={{flex: 1}}
keyboardVerticalOffset={65}
keyboardVerticalOffset={Platform.OS === 'ios' ? 65 : 0}
>
<StatusBar barStyle='light-content'/>
<TouchableWithoutFeedback onPress={this.blur}>
<View style={[GlobalStyles.container, GlobalStyles.signupContainer]}>
<Image

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 'app/selectors/preferences';
import LoginOptions from './login_options';
function mapStateToProps(state, ownProps) {
const {config, license} = state.entities.general;
return {
...ownProps,
config,
license,
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(LoginOptions);

View file

@ -1,9 +1,11 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent, PropTypes} from 'react';
import React, {PropTypes, PureComponent} from 'react';
import {injectIntl, intlShape} from 'react-intl';
import {
Image,
StatusBar,
Text,
View
} from 'react-native';
@ -15,23 +17,54 @@ import {preventDoubleTap} from 'app/utils/tap';
import logo from 'assets/images/logo.png';
export default class LoginOptions extends PureComponent {
class LoginOptions extends PureComponent {
static propTypes = {
actions: React.PropTypes.shape({
goToLogin: PropTypes.func.isRequired,
goToSaml: PropTypes.func.isRequired
}).isRequired,
intl: intlShape.isRequired,
navigator: PropTypes.object,
config: PropTypes.object.isRequired,
license: PropTypes.object.isRequired
license: PropTypes.object.isRequired,
theme: PropTypes.object
};
goToLogin = () => {
const {intl, navigator, theme} = this.props;
navigator.push({
screen: 'Login',
title: intl.formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'}),
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
}
});
};
goToSaml = () => {
const {intl, navigator, theme} = this.props;
navigator.push({
screen: 'SAML',
title: intl.formatMessage({id: 'mobile.routes.saml', defaultMessage: 'Single SignOn'}),
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
}
});
};
renderEmailOption = () => {
const config = this.props.config;
const {config} = this.props;
if (config.EnableSignInWithEmail === 'true' || config.EnableSignInWithUsername === 'true') {
return (
<Button
key='email'
onPress={() => preventDoubleTap(this.props.actions.goToLogin)}
onPress={() => preventDoubleTap(this.goToLogin, this)}
containerStyle={[GlobalStyles.signupButton, {backgroundColor: '#2389d7'}]}
>
<FormattedText
@ -52,7 +85,7 @@ export default class LoginOptions extends PureComponent {
return (
<Button
key='saml'
onPress={() => preventDoubleTap(this.props.actions.goToSaml)}
onPress={() => preventDoubleTap(this.goToSaml, this)}
containerStyle={[GlobalStyles.signupButton, {backgroundColor: '#34a28b'}]}
>
<Text
@ -70,6 +103,7 @@ export default class LoginOptions extends PureComponent {
render() {
return (
<View style={[GlobalStyles.container, GlobalStyles.signupContainer]}>
<StatusBar barStyle='light-content'/>
<Image
source={logo}
/>
@ -92,3 +126,5 @@ export default class LoginOptions extends PureComponent {
);
}
}
export default injectIntl(LoginOptions);

View file

@ -2,10 +2,8 @@
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import navigationSceneConnect from '../navigationSceneConnect';
import {goBack} from 'app/actions/navigation';
import {login} from 'mattermost-redux/actions/users';
import Mfa from './mfa';
@ -23,10 +21,9 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goBack,
login
}, dispatch)
};
}
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(Mfa);
export default connect(mapStateToProps, mapDispatchToProps)(Mfa);

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, {PropTypes, PureComponent} from 'react';
import {
ActivityIndicator,
Image,
@ -22,15 +22,15 @@ import logo from 'assets/images/logo.png';
import RequestStatus from 'mattermost-redux/constants/request_status';
export default class Mfa extends Component {
export default class Mfa extends PureComponent {
static propTypes = {
actions: React.PropTypes.shape({
goBack: React.PropTypes.func.isRequired,
login: React.PropTypes.func.isRequired
navigator: PropTypes.object,
actions: PropTypes.shape({
login: PropTypes.func.isRequired
}).isRequired,
loginId: React.PropTypes.string.isRequired,
password: React.PropTypes.string.isRequired,
loginRequest: React.PropTypes.object.isRequired
loginId: PropTypes.string.isRequired,
password: PropTypes.string.isRequired,
loginRequest: PropTypes.object.isRequired
};
constructor(props) {
@ -52,7 +52,7 @@ export default class Mfa extends Component {
// In case the login is successful the previous scene (login) will take care of the transition
if (this.props.loginRequest.status === RequestStatus.STARTED &&
nextProps.loginRequest.status === RequestStatus.FAILURE) {
this.props.actions.goBack();
this.props.navigator.pop({animated: true});
}
}
@ -86,8 +86,10 @@ export default class Mfa extends Component {
if (!this.state.token) {
this.setState({
error: {
id: 'login_mfa.tokenReq',
defaultMessage: 'Please enter an MFA token'
intl: {
id: 'login_mfa.tokenReq',
defaultMessage: 'Please enter an MFA token'
}
}
});
return;
@ -127,7 +129,7 @@ export default class Mfa extends Component {
<KeyboardAvoidingView
behavior='padding'
style={{flex: 1}}
keyboardVerticalOffset={0}
keyboardVerticalOffset={5}
>
<TouchableWithoutFeedback onPress={this.blur}>
<View style={[GlobalStyles.container, GlobalStyles.signupContainer]}>

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