Compare commits

..

42 Commits

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

* Feedback review

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

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

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

* Fixed view store blacklist

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

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

* Removed unused prop from ChannelDrawer

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

* Multiple performance improvements

* Feedback review

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

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

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

* RN-349 Mark channels as read when switching teams

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

View File

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

View File

@@ -1,48 +1,58 @@
[ignore]
; We fork some components by platform
# We fork some components by platform.
.*/*[.]android.js
; Ignore "BUCK" generated dirs
# Ignore templates with `@flow` in header
.*/local-cli/generator.*
# Ignore malformed json
.*/node_modules/y18n/test/.*\.json
# Ignore the website subdir
<PROJECT_ROOT>/website/.*
# Ignore BUCK generated dirs
<PROJECT_ROOT>/\.buckd/
; Ignore unexpected extra "@providesModule"
.*/node_modules/.*/node_modules/fbjs/.*
# Ignore unexpected extra @providesModule
.*/node_modules/commoner/test/source/widget/share.js
; Ignore duplicate module providers
; For RN Apps installed via npm, "Libraries" folder is inside
; "node_modules/react-native" but in the source repo it is in the root
# Ignore duplicate module providers
# For RN Apps installed via npm, "Libraries" folder is inside node_modules/react-native but in the source repo it is in the root
.*/Libraries/react-native/React.js
; Ignore polyfills
.*/Libraries/polyfills/.*
.*/Libraries/react-native/ReactNative.js
.*/node_modules/jest-runtime/build/__tests__/.*
[include]
[libs]
node_modules/react-native/Libraries/react-native/react-native-interface.js
node_modules/react-native/flow/
node_modules/react-native/flow
flow/
[options]
emoji=true
module.system=haste
esproposal.class_static_fields=enable
esproposal.class_instance_fields=enable
experimental.strict_type_args=true
munge_underscores=true
module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub'
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
suppress_type=$FlowIssue
suppress_type=$FlowFixMe
suppress_type=$FlowFixMeProps
suppress_type=$FlowFixMeState
suppress_type=$FixMe
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-2]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-2]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError
unsafe.enable_getters_and_setters=true
[version]
^0.56.0
^0.32.0

30
.gitignore vendored
View File

@@ -1,8 +1,5 @@
assets/override
dist
*.zip
server.PID
mattermost.keystore
# OSX
#
@@ -29,27 +26,28 @@ DerivedData
*.xcuserstate
project.xcworkspace
# Android/IntelliJ
# Android/IJ
#
build/
*.iml
.idea
.gradle
local.properties
*.iml
# node.js
#
node_modules/
npm-debug.log
.npminstall
yarn-error.log
# yarn
#
.yarninstall
# BUCK
buck-out/
\.buckd/
android/app/libs
*.keystore
android/keystores/debug.keystore
# Vim
[._]*.s[a-w][a-z]
@@ -60,17 +58,10 @@ Session.vim
*~
tags
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/
fastlane/report.xml
fastlane/Preview.html
*/fastlane/screenshots
*.zip
server.PID
mattermost.keystore
# Sentry
android/sentry.properties
@@ -82,6 +73,3 @@ ios/sentry.properties
# Pods
.podinstall
ios/Pods/
#editor-settings
.vscode

View File

@@ -1,88 +1,5 @@
# Mattermost Mobile Apps Changelog
## v1.5.2 Release
- Release Date: January 12, 2018
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue causing some Android devices to crash on launch
- Fixed an issue with the app occasionally crashing when receiving push notifications in a new channel
- Channel footer area is now refreshed when switching between Group and Direct Message channels
- Fixed an issue on some Android devices so Mattermost verifies it has permissions to access ringtones
- Fixed an issue where the text box overlapped the keyboard on some iOS devices using multiple keyboard layouts
- Fixed an issue with video uploads on Android devices
- Fixed an issue with GIF uploads on iOS devices
- Fixed an issue with the mention badge flickering on the channel drawer icon when there were over 10 unread mentions
- Fixed an issue with the app occasionally freezing when requesting the RefreshToken
## v1.5.1 Release
- Release Date: December 7, 2017
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed an issue with the upgrade app screen showing with a transparent background
- Fixed an issue with clearing or replying to notifications sometimes crashing the app on Android
- Fixed an issue with the app sometimes crashing due to a missing function in the swiping control
## v1.5 Release
- Release Date: December 6, 2017
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### File Viewer
- Preview videos, RTF, PDFs, Word, Excel, and Powerpoint files
#### iPhone X Compatibility
- Added support for iPhone X
#### Slash Commands
- Added support for using custom slash commands
- Added support for built-in slash commands /away, /online, /offline, /dnd, /header, /purpose, /kick, /me, /shrug
### Improvements
- In iOS, 3D touch can now be used to peek into a channel to view the contents, and quickly mark it as read
- Markdown images in posts now render
- Copy posts, URLs, and code blocks
- Opening a channel with Unread messages takes you to the "New Messages" indicator
- Support for data retention, interactive message buttons, and viewing Do Not Disturb statuses depending on the server version
- (Edited) indicator now shows up beside edited posts
- Added a "Recently Used" section for emoji reactions
### Bug Fixes
- Android notifications now follow the default system setting for vibration
- Fixed app crashing when opening notification settings on Android
- Fixed an issue where the "Proceed" button on sign in screen stopped working after pressing logout multiple times
- HEIC images posted from iPhones now get converted to JPEG before uploading
## v1.4.1 Release
Release Date: Nov 15, 2017
Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Bug Fixes
- Fixed network detection issue causing some people to be unable to access the app
- Fixed issue with lag when pressing send button
- Fixed app crash when opening notification settings
- Fixed various other bugs to reduce app crashes
## v1.4 Release
- Release Date: November 6, 2017
- Server Versions Supported: Server v4.0+ is required, Self-Signed SSL Certificates are not supported
### Highlights
#### Performance improvements
- Various performance improvements to decrease channel load times
### Bug Fixes
- Fixed issue with Android app sometimes showing a white screen when re-opening the app
- Fixed an issue with orientation lock not working on Android
## v1.3 Release
- Release Date: October 5, 2017

View File

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

14
Jenkinsfile vendored
View File

@@ -1,14 +0,0 @@
pipeline {
agent any
stages {
stage('Test') {
steps {
echo 'assets/base/config.json'
sh 'cat assets/base/config.json'
sh 'touch .podinstall'
sh 'make test || exit 1'
}
}
}
}

241
Makefile
View File

@@ -1,14 +1,11 @@
.PHONY: pre-run clean
.PHONY: check-style
.PHONY: start stop
.PHONY: run run-ios run-android
.PHONY: build-ios build-android unsigned-ios unsigned-android
.PHONY: test help
.PHONY: run run-ios run-android check-style test clean post-install start stop
.PHONY: check-ios-target build-ios
.PHONY: check-android-target prepare-android-build build-android
.PHONY: start-packager stop-packager
ios_target := $(filter-out build-ios,$(MAKECMDGOALS))
android_target := $(filter-out build-android,$(MAKECMDGOALS))
POD := $(shell command -v pod 2> /dev/null)
OS := $(shell sh -c 'uname -s 2>/dev/null')
BASE_ASSETS = $(shell find assets/base -type d) $(shell find assets/base -type f -name '*')
OVERRIDE_ASSETS = $(shell find assets/override -type d 2> /dev/null) $(shell find assets/override -type f -name '*' 2> /dev/null)
.yarninstall: package.json
@if ! [ $(shell command -v yarn 2> /dev/null) ]; then \
@@ -22,7 +19,6 @@ OVERRIDE_ASSETS = $(shell find assets/override -type d 2> /dev/null) $(shell fin
@touch $@
.podinstall:
ifeq ($(OS), Darwin)
ifdef POD
@echo Getting Cocoapods dependencies;
@cd ios && pod install;
@@ -30,10 +26,13 @@ else
@echo "Cocoapods is not installed https://cocoapods.org/"
@exit 1
endif
endif
@touch $@
BASE_ASSETS = $(shell find assets/base -type d) $(shell find assets/base -type f -name '*')
OVERRIDE_ASSETS = $(shell find assets/override -type d 2> /dev/null) $(shell find assets/override -type f -name '*' 2> /dev/null)
dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
@mkdir -p dist
@if [ -e dist/assets ] ; then \
@@ -43,14 +42,60 @@ dist/assets: $(BASE_ASSETS) $(OVERRIDE_ASSETS)
@echo "Generating app assets"
@node scripts/make-dist-assets.js
pre-run: | .yarninstall .podinstall dist/assets ## Installs dependencies and assets
pre-run: | .yarninstall .podinstall dist/assets
check-style: .yarninstall ## Runs eslint
run: run-ios
start: | pre-run start-packager
stop: stop-packager
check-device-ios:
@if ! [ $(shell command -v xcodebuild) ]; then \
@echo "xcode is not installed"; \
@exit 1; \
fi
@if ! [ $(shell command -v watchman) ]; then \
@echo "watchman is not installed"; \
@exit 1; \
fi
run-ios: | check-device-ios start
@echo Running iOS app in development
@react-native run-ios --simulator="${SIMULATOR}"
check-device-android:
@if ! [ $(ANDROID_HOME) ]; then \
@echo "ANDROID_HOME is not set"; \
@exit 1; \
fi
@if ! [ $(shell command -v adb 2> /dev/null) ]; then \
@echo "adb is not installed"; \
@exit 1; \
fi
ifneq ($(shell adb get-state),device)
@echo "no android device or emulator is running"
@exit 1;
endif
@if ! [ $(shell command -v watchman 2> /dev/null) ]; then \
@echo "watchman is not installed"; \
@exit 1; \
fi
run-android: | check-device-android start prepare-android-build
@echo Running Android app in development
@react-native run-android --no-packager
test: pre-run
@yarn test
check-style: .yarninstall
@echo Checking for style guide compliance
@node_modules/.bin/eslint --ext \".js\" --ignore-pattern node_modules --quiet .
clean: ## Cleans dependencies, previous builds and temp files
clean:
@echo Cleaning started
@yarn cache clean
@rm -rf node_modules
@rm -f .yarninstall
@@ -59,6 +104,7 @@ clean: ## Cleans dependencies, previous builds and temp files
@rm -rf ios/build
@rm -rf ios/Pods
@rm -rf android/app/build
@echo Cleanup finished
post-install:
@@ -74,153 +120,94 @@ post-install:
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-messageformat/package.json
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
@sed -i'' -e 's|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_FULL_USER);|g' node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/params/Orientation.java
@sed -i'' -e "s|var AndroidTextInput = requireNativeComponent('AndroidTextInput', null);|var AndroidTextInput = requireNativeComponent('CustomTextInput', null);|g" node_modules/react-native/Libraries/Components/TextInput/TextInput.js
@if [ $(shell grep "const Platform" node_modules/react-native/Libraries/Lists/VirtualizedList.js | grep -civ grep) -eq 0 ]; then \
sed $ -i'' -e "s|const ReactNative = require('ReactNative');|const ReactNative = require('ReactNative');`echo $\\\\\\r;`const Platform = require('Platform');|g" node_modules/react-native/Libraries/Lists/VirtualizedList.js; \
fi
@sed -i'' -e 's|transform: \[{scaleY: -1}\],|...Platform.select({android: {transform: \[{perspective: 1}, {scaleY: -1}\]}, ios: {transform: \[{scaleY: -1}\]}}),|g' node_modules/react-native/Libraries/Lists/VirtualizedList.js
@cd ./node_modules/react-native-svg/ios && rm -rf PerformanceBezier QuartzBookPack && yarn run postinstall
@cd ./node_modules/mattermost-redux && yarn run build
@sed -i'' -e 's|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_SENSOR);|g' node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/params/Orientation.java
start: | pre-run ## Starts the React Native packager server
start-packager:
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
node ./node_modules/react-native/local-cli/cli.js start; \
node ./node_modules/react-native/local-cli/cli.js start --reset-cache & echo $$! > server.PID; \
else \
echo React Native packager server already running; \
ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' > server.PID; \
ps -e | grep -i "cli.js start" | grep -v grep | awk '{print $$1}' > server.PID; \
fi
stop: ## Stops the React Native packager server
stop-packager:
@echo Stopping React Native packager server
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 1 ]; then \
ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9; \
@if [ -e "server.PID" ] ; then \
kill -9 `cat server.PID` && rm server.PID; \
echo React Native packager server stopped; \
else \
echo No React Native packager server running; \
fi
check-device-ios:
@if ! [ $(shell command -v xcodebuild) ]; then \
echo "xcode is not installed"; \
exit 1; \
fi
@if ! [ $(shell command -v watchman) ]; then \
echo "watchman is not installed"; \
exit 1; \
fi
check-ios-target:
ifeq ($(ios_target), )
@echo No target set to build iOS app
@echo "Try running make build-ios TARGET where TARGET is one of dev, beta or release"
@exit 1
endif
ifneq ($(ios_target), $(filter $(ios_target),dev beta release))
@echo Invalid target set to build iOS app
@echo "Try running make build-ios TARGET where TARGET is one of dev, beta or release"
@exit 1
endif
check-device-android:
@if ! [ $(ANDROID_HOME) ]; then \
@echo "ANDROID_HOME is not set"; \
@exit 1; \
fi
@if ! [ $(shell command -v adb 2> /dev/null) ]; then \
@echo "adb is not installed"; \
@exit 1; \
fi
do-build-ios:
@echo "Building ios $(ios_target) app"
@cd fastlane && NODE_ENV=production bundle exec fastlane ios $(ios_target)
@echo "Connect your Android device or open the emulator"
@adb wait-for-device
@if ! [ $(shell command -v watchman 2> /dev/null) ]; then \
@echo "watchman is not installed"; \
@exit 1; \
fi
build-ios: | check-ios-target pre-run check-style start-packager do-build-ios stop-packager
check-android-target:
ifeq ($(android_target), )
@echo No target set to build Android app
@echo "Try running make build-android TARGET where TARGET is one of dev, beta or release"
@exit 1
endif
ifneq ($(android_target), $(filter $(android_target),dev alpha release))
@echo Invalid target set to build Android app
@echo "Try running make build-android TARGET where TARGET is one of dev, beta or release"
@exit 1
endif
prepare-android-build:
@rm -rf ./node_modules/react-native/local-cli/templates/HelloWorld
@rm -rf ./node_modules/react-native-linear-gradient/Examples/
@rm -rf ./node_modules/react-native-orientation/demo/
run: run-ios ## alias for run-ios
do-build-android:
@echo "Building android $(android_target) app"
@cd fastlane && NODE_ENV=production bundle exec fastlane android $(android_target)
run-ios: | check-device-ios pre-run ## Runs the app on an iOS simulator
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
node ./node_modules/react-native/local-cli/cli.js start & echo Running iOS app in development; \
react-native run-ios --simulator="${SIMULATOR}"; \
wait; \
else \
echo Running iOS app in development; \
react-native run-ios --simulator="${SIMULATOR}"; \
fi
build-android: | check-android-target pre-run check-style start-packager prepare-android-build do-build-android stop-packager
run-android: | check-device-android pre-run prepare-android-build ## Runs the app on an Android emulator or dev device
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
node ./node_modules/react-native/local-cli/cli.js start & echo Running Android app in development; \
if [ ! -z ${VARIANT} ]; then \
react-native run-android --no-packager --variant=${VARIANT}; \
else \
react-native run-android --no-packager; \
fi; \
wait; \
else \
echo Running Android app in development; \
if [ ! -z ${VARIANT} ]; then \
react-native run-android --no-packager --variant=${VARIANT}; \
else \
react-native run-android --no-packager; \
fi; \
fi
build-ios: | pre-run check-style ## Creates an iOS build
ifneq ($(IOS_APP_GROUP),)
@mkdir -p assets/override
@echo "{\n\t\"AppGroupId\": \"$$IOS_APP_GROUP\"\n}" > assets/override/config.json
endif
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
node ./node_modules/react-native/local-cli/cli.js start & echo; \
fi
@echo "Building iOS app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane ios build
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
@rm -rf assets/override
build-android: | pre-run check-style prepare-android-build ## Creates an Android build
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
node ./node_modules/react-native/local-cli/cli.js start & echo; \
fi
@echo "Building Android app"
@cd fastlane && BABEL_ENV=production NODE_ENV=production bundle exec fastlane android build
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
unsigned-ios: pre-run check-style
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
node ./node_modules/react-native/local-cli/cli.js start & echo; \
fi
do-unsigned-ios:
@echo "Building unsigned iOS app"
ifneq ($(IOS_APP_GROUP),)
@mkdir -p assets/override
@echo "{\n\t\"AppGroupId\": \"$$IOS_APP_GROUP\"\n}" > assets/override/config.json
endif
@cd fastlane && NODE_ENV=production bundle exec fastlane ios unsigned
@mkdir -p build-ios
@cd ios/ && xcodebuild -workspace Mattermost.xcworkspace/ -scheme Mattermost -sdk iphoneos -configuration Relase -parallelizeTargets -resultBundlePath ../build-ios/result -derivedDataPath ../build-ios/ CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO
@cd build-ios/ && mkdir -p Payload && cp -R Build/Products/Release-iphoneos/Mattermost.app Payload/ && zip -r Mattermost-unsigned.ipa Payload/
@mv build-ios/Mattermost-unsigned.ipa .
@rm -rf build-ios/
@rm -rf assets/override
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
unsigned-android: pre-run check-style prepare-android-build
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \
echo Starting React Native packager server; \
node ./node_modules/react-native/local-cli/cli.js start & echo; \
fi
do-unsigned-android:
@echo "Building unsigned Android app"
@cd fastlane && NODE_ENV=production bundle exec fastlane android unsigned
@mv android/app/build/outputs/apk/app-unsigned-unsigned.apk ./Mattermost-unsigned.apk
@ps -e | grep -i "cli.js start" | grep -iv grep | awk '{print $$1}' | xargs kill -9
test: | pre-run check-style ## Runs tests
@yarn test
unsigned-android: pre-run check-style start-packager do-unsigned-android stop-packager
## Help documentation https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
unsigned-ios: pre-run check-style start-packager do-unsigned-ios stop-packager
alpha:
@:
dev:
@:
beta:
@:
release:
@:

1277
NOTICE.txt

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,9 @@
**Supported iOS versions:** 9.3+
**Supported Android versions:** 5.0+
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 11 languages. Learn more at [https://about.mattermost.com](https://about.mattermost.com).
Mattermost is an open source Slack-alternative used by thousands of companies around the world in 11 languages. Learn more at https://mattermost.com.
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or build them yourself.
You can download our apps from the [App Store](https://about.mattermost.com/mattermost-ios-app/) or [Google Play Store](https://about.mattermost.com/mattermost-android-app/), or package them yourself.
We plan on releasing monthly updates with new features - check the [changelog](https://github.com/mattermost/mattermost-mobile/blob/master/CHANGELOG.md) for what features are currently supported!
@@ -36,7 +36,84 @@ To help with testing app updates before they're released, you can:
3. Follow [these instructions](https://docs.mattermost.com/developer/mobile-developer-setup.html) to set up your developer environment
4. Join the [Native Mobile Apps channel](https://pre-release.mattermost.com/core/channels/native-mobile-apps) on our team site to ask questions
# Installing Dependencies
Follow the [React Native Getting Started Guide](https://facebook.github.io/react-native/docs/getting-started.html) for detailed instructions on setting up your local machine for development.
# Detailed configuration:
## Mac
- General requirements
- XCode 8.3
- Install required packages using homebrew:
```bash
$ brew install watchman
$ brew install yarn
```
- Clone repository and configure:
```bash
$ git clone git@github.com:mattermost/mattermost-mobile.git
$ cd mattermost-mobile
$ npm install
$ npm install -g react-native-cli
```
- Run application
```bash
$ make run
```
- Stop the packager server
```bash
$ make stop
```
## Linux:
- General requiriments:
- JDK 7 or greater
- Android SDK
- Virtualbox
- An Android emulator: Genymotion or Android emulator. If using genymotion ensure that it uses existing adb tools (Settings: "Use custom Android SDK Tools")
- Install watchman (do this globally):
```bash
$ git clone https://github.com/facebook/watchman.git
$ cd watchman
$ git checkout master
$ ./autogen.sh
$ ./configure make
$ sudo make install
```
Configure your kernel to accept a lot of file watches, using a command like:
```bash
$ sudo sysctl -w fs.inotify.max_user_watches=1048576
```
- Clone repository and configure:
```bash
$ git clone git@github.com:mattermost/mattermost-mobile.git
$ cd mattermost-mobile
$ npm install
$ npm install -g react-native-cli
```
- You can create a file named `assets/override/config.json` and add the url to the Mattermost server that you will use to develop:
`{
"DefaultServerUrl": "https://pre-release.mattermost.com"
}`
To use a local Mattermost server you will need to configure the "DefaultServerUrl" depending on the emulator you will use:
* IOs: "DefaultServerUrl": "http://localhost:8065"
* Android: "DefaultServerUrl": "http://10.0.2.2:3000"
* Genymotion: "DefaultServerUrl": "http://10.0.3.2:8065"
- Run application
- Start emulator
- Start react packager: `$ react-native start`
- Run in emulator: `$ react-native run-android`
# Frequently Asked Questions
@@ -71,3 +148,11 @@ If your app is working properly, you should see a grey “Connecting…” bar t
If you are seeing this message all the time, and your internet connection seems fine:
Ask your server administrator if the server uses NGINX or another webserver as a reverse proxy. If so, they should check that it is configured correctly for [supporting the websocket connection for APIv4 endpoints](https://docs.mattermost.com/install/install-ubuntu-1604.html#configuring-nginx-as-a-proxy-for-mattermost-server).
# Issues building app for own device using make build-*
That command is an internal pipeline command for mattermost mobile to publish the mobile apps to ````Apple App Store```` and ````Google Play Store````. All ````make build-*```` commands should be avoided for this reason.
To build the modified react native client use the instructions for [Running on Device](http://facebook.github.io/react-native/docs/running-on-device.html) from the [React Native Guide](https://facebook.github.io/react-native/docs/getting-started.html).

View File

@@ -1,3 +1,5 @@
import re
# To learn about Buck see [Docs](https://buckbuild.com/).
# To run your application with Buck:
# - install Buck
@@ -9,9 +11,8 @@
#
lib_deps = []
for jarfile in glob(['libs/*.jar']):
name = 'jars__' + jarfile[jarfile.rindex('/') + 1: jarfile.rindex('.jar')]
name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile)
lib_deps.append(':' + name)
prebuilt_jar(
name = name,
@@ -19,7 +20,7 @@ for jarfile in glob(['libs/*.jar']):
)
for aarfile in glob(['libs/*.aar']):
name = 'aars__' + aarfile[aarfile.rindex('/') + 1: aarfile.rindex('.aar')]
name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile)
lib_deps.append(':' + name)
android_prebuilt_aar(
name = name,
@@ -27,39 +28,39 @@ for aarfile in glob(['libs/*.aar']):
)
android_library(
name = "all-libs",
exported_deps = lib_deps,
name = 'all-libs',
exported_deps = lib_deps
)
android_library(
name = "app-code",
srcs = glob([
"src/main/java/**/*.java",
]),
deps = [
":all-libs",
":build_config",
":res",
],
name = 'app-code',
srcs = glob([
'src/main/java/**/*.java',
]),
deps = [
':all-libs',
':build_config',
':res',
],
)
android_build_config(
name = "build_config",
package = "com.mattermost-mobile",
name = 'build_config',
package = 'com.mattermost.rnbeta',
)
android_resource(
name = "res",
res = "src/main/res",
package = "com.mattermost.rnbeta",
name = 'res',
res = 'src/main/res',
package = 'com.mattermost.rnbeta',
)
android_binary(
name = "app",
keystore = "//android/keystores:debug",
manifest = "src/main/AndroidManifest.xml",
package_type = "debug",
deps = [
":app-code",
],
name = 'app',
package_type = 'debug',
manifest = 'src/main/AndroidManifest.xml',
keystore = '//android/keystores:debug',
deps = [
':app-code',
],
)

View File

@@ -33,13 +33,6 @@ import com.android.build.OutputFile
* // bundleInPaidRelease: true,
* // bundleInBeta: true,
*
* // whether to disable dev mode in custom build variants (by default only disabled in release)
* // for example: to disable dev mode in the staging build type (if configured)
* devDisabledInStaging: true,
* // The configuration property can be in the following formats
* // 'devDisabledIn${productFlavor}${buildType}'
* // 'devDisabledIn${buildType}'
*
* // the root of your project, i.e. where "package.json" lives
* root: "../../",
*
@@ -65,21 +58,17 @@ import com.android.build.OutputFile
* inputExcludes: ["android/**", "ios/**"],
*
* // override which node gets called and with what additional arguments
* nodeExecutableAndArgs: ["node"],
* nodeExecutableAndArgs: ["node"]
*
* // supply additional arguments to the packager
* extraPackagerArgs: []
* ]
*/
project.ext.react = [
entryFile: "index.js"
]
apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
if (System.getenv("SENTRY_ENABLED") == "true") {
if (System.getenv("MM_SENTRY_ENABLED") == "true") {
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
}
@@ -106,8 +95,8 @@ android {
applicationId "com.mattermost.rnbeta"
minSdkVersion 21
targetSdkVersion 23
versionCode 85
versionName "1.6.0"
versionCode 57
versionName "1.3.0"
multiDexEnabled true
ndk {
abiFilters "armeabi-v7a", "x86"
@@ -163,11 +152,8 @@ android {
}
dependencies {
compile project(':react-native-doc-viewer')
compile project(':react-native-video')
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:25.0.1"
compile 'com.android.support:percent:25.3.1'
compile "com.facebook.react:react-native:+" // From node_modules
compile project(':react-native-navigation')
compile project(':react-native-image-picker')

View File

@@ -50,10 +50,6 @@
-dontwarn com.facebook.react.**
# TextLayoutBuilder uses a non-public Android constructor within StaticLayout.
# See libs/proxy/src/main/java/com/facebook/fbui/textlayoutbuilder/proxy for details.
-dontwarn android.text.StaticLayout
# okhttp
-keepattributes Signature

View File

@@ -56,21 +56,6 @@
<activity
android:name="com.reactnativenavigation.controllers.NavigationActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"/>
<activity
android:noHistory="false"
android:name="com.mattermost.share.ShareActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
// for sharing
<data android:mimeType="*/*" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -1,30 +0,0 @@
package com.mattermost.components;
import android.content.Context;
import android.text.InputType;
import com.facebook.react.views.textinput.ReactEditText;
public class CustomTextInput extends ReactEditText {
private boolean autoScroll = false;
public CustomTextInput(Context context) {
super(context);
}
private boolean isMultiline() {
return (getInputType() & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
}
@Override
public boolean isLayoutRequested() {
if (isMultiline() && !autoScroll) {
return true;
}
return false;
}
public void setAutoScroll(boolean autoScroll) {
this.autoScroll = autoScroll;
}
}

View File

@@ -1,35 +0,0 @@
package com.mattermost.components;
import android.text.InputType;
import android.util.TypedValue;
import com.facebook.react.views.textinput.ReactTextInputManager;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewDefaults;
import com.facebook.react.uimanager.annotations.ReactProp;
public class CustomTextInputManager extends ReactTextInputManager {
@Override
public String getName() {
return "CustomTextInput";
}
@Override
public CustomTextInput createViewInstance(ThemedReactContext context) {
CustomTextInput editText = new CustomTextInput(context);
int inputType = editText.getInputType();
editText.setInputType(inputType & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
editText.setReturnKeyType("done");
editText.setTextSize(
TypedValue.COMPLEX_UNIT_PX,
(int) Math.ceil(PixelUtil.toPixelFromSP(ViewDefaults.FONT_SIZE_SP)));
return editText;
}
@ReactProp(name = "autoScroll", defaultBoolean = false)
public void setAutoScroll(CustomTextInput view, boolean autoScroll) {
view.setAutoScroll(autoScroll);
}
}

View File

@@ -16,11 +16,8 @@ import android.os.Build;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.RemoteInput;
import android.provider.Settings.System;
import java.util.LinkedHashMap;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
import java.lang.reflect.Field;
import com.wix.reactnativenotifications.core.notification.PushNotification;
@@ -43,7 +40,7 @@ public class CustomPushNotification extends PushNotification {
public static final String NOTIFICATION_REPLIED_EVENT_NAME = "notificationReplied";
private static LinkedHashMap<String,Integer> channelIdToNotificationCount = new LinkedHashMap<String,Integer>();
private static LinkedHashMap<String,List<Bundle>> channelIdToNotification = new LinkedHashMap<String,List<Bundle>>();
private static LinkedHashMap<String,ArrayList<Bundle>> channelIdToNotification = new LinkedHashMap<String,ArrayList<Bundle>>();
private static AppLifecycleFacade lifecycleFacade;
private static Context context;
@@ -56,21 +53,8 @@ public class CustomPushNotification extends PushNotification {
if (notificationId != -1) {
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
if (context != null) {
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
}
}
public static void clearNotification(Context mContext, int notificationId, String channelId) {
if (notificationId != -1) {
channelIdToNotificationCount.remove(channelId);
channelIdToNotification.remove(channelId);
if (mContext != null) {
final NotificationManager notificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(notificationId);
}
}
@@ -90,16 +74,14 @@ public class CustomPushNotification extends PushNotification {
channelIdToNotificationCount.put(channelId, count);
Object bundleArray = channelIdToNotification.get(channelId);
List list = null;
ArrayList list = null;
if (bundleArray == null) {
list = Collections.synchronizedList(new ArrayList(0));
list = new ArrayList(0);
} else {
list = Collections.synchronizedList((List)bundleArray);
}
synchronized (list) {
list.add(0, data);
channelIdToNotification.put(channelId, list);
list = (ArrayList)bundleArray;
}
list.add(0, data);
channelIdToNotification.put(channelId, list);
}
if ("clear".equals(type)) {
@@ -210,15 +192,9 @@ public class CustomPushNotification extends PushNotification {
String summaryTitle = String.format("%s (%d)", title, numMessages);
Notification.InboxStyle style = new Notification.InboxStyle();
List<Bundle> bundleArray = channelIdToNotification.get(channelId);
List<Bundle> list;
if (bundleArray != null) {
list = new ArrayList<Bundle>(bundleArray);
} else {
list = new ArrayList<Bundle>();
}
ArrayList<Bundle> list = (ArrayList<Bundle>) channelIdToNotification.get(channelId);
for (Bundle data : list) {
for (Bundle data : list){
String msg = data.getString("message");
if (msg != message) {
style.addLine(data.getString("message"));
@@ -278,14 +254,14 @@ public class CustomPushNotification extends PushNotification {
notification.setSound(Uri.parse(soundUri), AudioManager.STREAM_NOTIFICATION);
}
} else {
Uri defaultUri = System.DEFAULT_NOTIFICATION_URI;
Uri defaultUri = RingtoneManager.getActualDefaultRingtoneUri(mContext, RingtoneManager.TYPE_NOTIFICATION);
notification.setSound(defaultUri, AudioManager.STREAM_NOTIFICATION);
}
boolean vibrate = notificationPreferences.getShouldVibrate();
if (vibrate) {
// use the system default for vibration
notification.setDefaults(Notification.DEFAULT_VIBRATE);
// Each element then alternates between delay, vibrate, sleep, vibrate, sleep
notification.setVibrate(new long[] {1000, 1000, 500, 1000, 500});
}
boolean blink = notificationPreferences.getShouldBlink();

View File

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

View File

@@ -1,15 +1,11 @@
package com.mattermost.rnbeta;
import com.mattermost.share.SharePackage;
import android.app.Application;
import android.support.annotation.NonNull;
import android.content.Context;
import android.os.Bundle;
import com.facebook.react.ReactApplication;
import com.reactlibrary.RNReactNativeDocViewerPackage;
import com.brentvatne.react.ReactVideoPackage;
import com.horcrux.svg.SvgPackage;
import com.inprogress.reactnativeyoutube.ReactNativeYouTube;
import io.sentry.RNSentryPackage;
import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage;
@@ -27,6 +23,7 @@ import com.gnet.bottomsheet.RNBottomSheetPackage;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
import com.psykar.cookiemanager.CookieManagerPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.horcrux.svg.SvgPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.github.yamill.orientation.OrientationPackage;
import com.reactnativenavigation.NavigationApplication;
@@ -69,18 +66,10 @@ public class MainApplication extends NavigationApplication implements INotificat
new MattermostPackage(this),
new RNSentryPackage(this),
new ReactNativeExceptionHandlerPackage(),
new ReactNativeYouTube(),
new ReactVideoPackage(),
new RNReactNativeDocViewerPackage(),
new SharePackage()
new ReactNativeYouTube()
);
}
@Override
public String getJSMainModuleName() {
return "index";
}
@Override
public void onCreate() {
super.onCreate();

View File

@@ -10,8 +10,6 @@ import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.bridge.JavaScriptModule;
import com.mattermost.components.CustomTextInputManager;
public class MattermostPackage implements ReactPackage {
private final MainApplication mApplication;
@@ -29,8 +27,6 @@ public class MattermostPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new CustomTextInputManager()
);
return Collections.emptyList();
}
}

View File

@@ -4,23 +4,20 @@ import android.content.Context;
import android.content.Intent;
import android.app.IntentService;
import android.os.Bundle;
import android.util.Log;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationDismissService extends IntentService {
private Context mContext;
public NotificationDismissService() {
super("notificationDismissService");
}
@Override
protected void onHandleIntent(Intent intent) {
mContext = getApplicationContext();
Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent);
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
String channelId = bundle.getString("channel_id");
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
Log.i("ReactNative", "Dismiss notification");
CustomPushNotification.clearNotification(notificationId, channelId);
}
}

View File

@@ -69,9 +69,7 @@ public class NotificationPreferencesModule extends ReactContextBaseJavaModule {
}
Uri defaultUri = RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_NOTIFICATION);
if (defaultUri != null) {
result.putString("defaultUri", Uri.decode(defaultUri.toString()));
}
result.putString("defaultUri", Uri.decode(defaultUri.toString()));
result.putString("selectedUri", mNotificationPreference.getNotificationSound());
result.putBoolean("shouldVibrate", mNotificationPreference.getShouldVibrate());
result.putBoolean("shouldBlink", mNotificationPreference.getShouldBlink());

View File

@@ -6,7 +6,6 @@ import android.app.NotificationManager;
import android.app.RemoteInput;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
import com.facebook.react.HeadlessJsTaskService;
import com.facebook.react.bridge.Arguments;
@@ -16,11 +15,9 @@ import com.facebook.react.jstasks.HeadlessJsTaskConfig;
import com.wix.reactnativenotifications.core.NotificationIntentAdapter;
public class NotificationReplyService extends HeadlessJsTaskService {
private Context mContext;
@Override
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
mContext = getApplicationContext();
if (CustomPushNotification.KEY_TEXT_REPLY.equals(intent.getAction())) {
CharSequence message = getReplyMessage(intent);
@@ -30,9 +27,8 @@ public class NotificationReplyService extends HeadlessJsTaskService {
bundle.putInt("msg_count", CustomPushNotification.getMessageCountInChannel(channelId));
int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1);
CustomPushNotification.clearNotification(mContext, notificationId, channelId);
CustomPushNotification.clearNotification(notificationId, channelId);
Log.i("ReactNative", "Replying service");
return new HeadlessJsTaskConfig(
"notificationReplied",
Arguments.fromBundle(bundle),

View File

@@ -1,175 +0,0 @@
package com.mattermost.share;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.content.ContentUris;
import android.os.Environment;
import android.webkit.MimeTypeMap;
import android.os.ParcelFileDescriptor;
import java.io.*;
import java.nio.channels.FileChannel;
// Class based on the steveevers DocumentHelper https://gist.github.com/steveevers/a5af24c226f44bb8fdc3
public class RealPathUtil {
public static String getRealPathFromURI(final Context context, final Uri uri) {
final boolean isKitKatOrNewer = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKatOrNewer && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
} else if (isDownloadsDocument(uri)) {
// DownloadsProvider
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
} else if (isMediaDocument(uri)) {
// MediaProvider
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[] {
split[1]
};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
// MediaStore (and general)
if (isGooglePhotosUri(uri)) {
return uri.getLastPathSegment();
}
String path = getDataColumn(context, uri, null, null);
if (path != null) {
return path;
}
// Try save to tmp file, and return tmp file path
return getPathFromSavingTempFile(context, uri);
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
public static String getPathFromSavingTempFile(Context context, final Uri uri) {
File tmpFile;
try {
String fileName = uri.getLastPathSegment();
tmpFile = File.createTempFile("tmp", fileName, context.getCacheDir());
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r");
FileChannel src = new FileInputStream(pfd.getFileDescriptor()).getChannel();
FileChannel dst = new FileOutputStream(tmpFile).getChannel();
dst.transferFrom(src, 0, src.size());
src.close();
dst.close();
} catch (IOException ex) {
return null;
}
return tmpFile.getAbsolutePath();
}
public static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {
column
};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
public static boolean isGooglePhotosUri(Uri uri) {
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
}
public static String getExtension(String uri) {
if (uri == null) {
return null;
}
int dot = uri.lastIndexOf(".");
if (dot >= 0) {
return uri.substring(dot);
} else {
// No extension.
return "";
}
}
public static String getMimeType(File file) {
String extension = getExtension(file.getName());
if (extension.length() > 0)
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.substring(1));
return "application/octet-stream";
}
public static String getMimeType(String filePath) {
File file = new File(filePath);
return getMimeType(file);
}
}

View File

@@ -1,13 +0,0 @@
package com.mattermost.share;
import android.content.Intent;
import android.os.Bundle;
import com.facebook.react.ReactActivity;
public class ShareActivity extends ReactActivity {
@Override
protected String getMainComponentName() {
return "MattermostShare";
}
}

View File

@@ -1,199 +0,0 @@
package com.mattermost.share;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.Arguments;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.graphics.Bitmap;
import java.io.InputStream;
import java.io.File;
import java.util.ArrayList;
import javax.annotation.Nonnull;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONException;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.Response;
public class ShareModule extends ReactContextBaseJavaModule {
private final OkHttpClient client = new OkHttpClient();
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
public ShareModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "MattermostShare";
}
@ReactMethod
public void clear() {
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
Intent intent = currentActivity.getIntent();
intent.setAction("");
intent.removeExtra(Intent.EXTRA_TEXT);
intent.removeExtra(Intent.EXTRA_STREAM);
}
}
@ReactMethod
public void close(ReadableMap data) {
getCurrentActivity().finish();
if (data != null) {
ReadableArray files = data.getArray("files");
String serverUrl = data.getString("url");
String token = data.getString("token");
JSONObject postData = buildPostObject(data);
if (files.size() > 0) {
uploadFiles(serverUrl, token, files, postData);
} else {
try {
post(serverUrl, token, postData);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
@ReactMethod
public void data(Promise promise) {
promise.resolve(processIntent());
}
public WritableArray processIntent() {
WritableMap map = Arguments.createMap();
WritableArray items = Arguments.createArray();
String text = "";
String type = "";
String action = "";
Activity currentActivity = getCurrentActivity();
if (currentActivity != null) {
Intent intent = currentActivity.getIntent();
action = intent.getAction();
type = intent.getType();
if (type == null) {
type = "";
}
if (Intent.ACTION_SEND.equals(action) && "text/plain".equals(type)) {
text = intent.getStringExtra(Intent.EXTRA_TEXT);
map.putString("value", text);
map.putString("type", type);
items.pushMap(map);
} else if (Intent.ACTION_SEND.equals(action)) {
Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
text = "file://" + RealPathUtil.getRealPathFromURI(currentActivity, uri);
map.putString("value", text);
map.putString("type", type);
items.pushMap(map);
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
for (Uri uri : uris) {
String filePath = RealPathUtil.getRealPathFromURI(currentActivity, uri);
map = Arguments.createMap();
text = "file://" + filePath;
map.putString("value", text);
map.putString("type", RealPathUtil.getMimeType(filePath));
items.pushMap(map);
}
}
}
return items;
}
private JSONObject buildPostObject(ReadableMap data) {
JSONObject json = new JSONObject();
try {
json.put("user_id", data.getString("currentUserId"));
json.put("channel_id", data.getString("channelId"));
json.put("message", data.getString("value"));
} catch (JSONException e) {
e.printStackTrace();
}
return json;
}
private void post(String serverUrl, String token, JSONObject postData) throws IOException {
RequestBody body = RequestBody.create(JSON, postData.toString());
Request request = new Request.Builder()
.header("Authorization", "BEARER " + token)
.url(serverUrl + "/api/v4/posts")
.post(body)
.build();
Response response = client.newCall(request).execute();
}
private void uploadFiles(String serverUrl, String token, ReadableArray files, JSONObject postData) {
try {
MultipartBody.Builder builder = new MultipartBody.Builder()
.setType(MultipartBody.FORM);
for(int i = 0 ; i < files.size() ; i++) {
ReadableMap file = files.getMap(i);
String filePath = file.getString("fullPath").replaceFirst("file://", "");
File fileInfo = new File(filePath);
if (fileInfo.exists()) {
final MediaType MEDIA_TYPE = MediaType.parse(file.getString("mimeType"));
builder.addFormDataPart("files", file.getString("filename"), RequestBody.create(MEDIA_TYPE, fileInfo));
}
}
builder.addFormDataPart("channel_id", postData.getString("channel_id"));
RequestBody body = builder.build();
Request request = new Request.Builder()
.header("Authorization", "BEARER " + token)
.url(serverUrl + "/api/v4/files")
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
if (response.isSuccessful()) {
String responseData = response.body().string();
JSONObject responseJson = new JSONObject(responseData);
JSONArray fileInfoArray = responseJson.getJSONArray("file_infos");
JSONArray file_ids = new JSONArray();
for(int i = 0 ; i < fileInfoArray.length() ; i++) {
JSONObject fileInfo = fileInfoArray.getJSONObject(i);
file_ids.put(fileInfo.getString("id"));
}
postData.put("file_ids", file_ids);
post(serverUrl, token, postData);
}
} catch (IOException e) {
e.printStackTrace();
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}

View File

@@ -1,27 +0,0 @@
package com.mattermost.share;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.ReactPackage;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class SharePackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new ShareModule(reactContext));
}
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
keystore(
name = "debug",
properties = "debug.keystore.properties",
store = "debug.keystore",
visibility = [
"PUBLIC",
],
name = 'debug',
store = 'debug.keystore',
properties = 'debug.keystore.properties',
visibility = [
'PUBLIC',
],
)

View File

@@ -1,8 +1,4 @@
rootProject.name = 'Mattermost'
include ':react-native-doc-viewer'
project(':react-native-doc-viewer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-doc-viewer/android')
include ':react-native-video'
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
include ':react-native-youtube'
project(':react-native-youtube').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-youtube/android')
include ':react-native-sentry'
@@ -31,9 +27,9 @@ include ':reactnativenotifications'
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android')
include ':app'
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-orientation'
project(':react-native-orientation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-orientation/android')
include ':react-native-linear-gradient'
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')

View File

@@ -1,4 +0,0 @@
{
"name": "Mattermost",
"displayName": "Mattermost"
}

View File

@@ -8,9 +8,10 @@ import {ViewTypes} from 'app/constants';
import {UserTypes} from 'mattermost-redux/action_types';
import {
fetchMyChannelsAndMembers,
markChannelAsRead,
getChannelStats,
selectChannel,
leaveChannel as serviceLeaveChannel
leaveChannel as serviceLeaveChannel,
unfavoriteChannel
} from 'mattermost-redux/actions/channels';
import {getPosts, getPostsBefore, getPostsSince, getPostThread} from 'mattermost-redux/actions/posts';
import {getFilesForPost} from 'mattermost-redux/actions/files';
@@ -18,20 +19,18 @@ import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
import {General, Preferences} from 'mattermost-redux/constants';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {
getChannelByName,
getDirectChannelName,
getUserIdFromChannelName,
isDirectChannelVisible,
isGroupChannelVisible,
isDirectChannel,
isGroupChannel
} from 'mattermost-redux/utils/channel_utils';
import {getLastCreateAt} from 'mattermost-redux/utils/post_utils';
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
import {isDirectChannelVisible, isGroupChannelVisible} from 'app/utils/channels';
const MAX_POST_TRIES = 3;
export function loadChannelsIfNecessary(teamId) {
@@ -181,17 +180,15 @@ export function loadPostsIfNecessaryWithRetry(channelId) {
};
}
export async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_POST_TRIES) {
async function retryGetPostsAction(action, dispatch, getState, maxTries = MAX_POST_TRIES) {
for (let i = 0; i < maxTries; i++) {
const {data} = await action(dispatch, getState);
const posts = await action(dispatch, getState);
if (data) {
dispatch(setChannelRetryFailed(false));
return data;
if (posts) {
return posts;
}
}
dispatch(setChannelRetryFailed(true));
return null;
}
@@ -224,8 +221,7 @@ export function selectInitialChannel(teamId) {
const {channels, myMembers} = state.entities.channels;
const {currentUserId} = state.entities.users;
const {myPreferences} = state.entities.preferences;
const lastChannelForTeam = state.views.team.lastChannelForTeam[teamId];
const lastChannelId = lastChannelForTeam && lastChannelForTeam.length ? lastChannelForTeam[0] : '';
const lastChannelId = state.views.team.lastChannelForTeam[teamId] || '';
const lastChannel = channels[lastChannelId];
const isDMVisible = lastChannel && lastChannel.type === General.DM_CHANNEL &&
@@ -237,27 +233,20 @@ export function selectInitialChannel(teamId) {
if (lastChannelId && myMembers[lastChannelId] &&
(lastChannel.team_id === teamId || isDMVisible || isGMVisible)) {
handleSelectChannel(lastChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId)(dispatch, getState);
return;
}
const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL);
let channelId;
if (channel) {
channelId = channel.id;
dispatch(setChannelDisplayName(''));
handleSelectChannel(channel.id)(dispatch, getState);
} else {
// Handle case when the default channel cannot be found
// so we need to get the first available channel of the team
const channelsInTeam = Object.values(channels).filter((c) => c.team_id === teamId);
const firstChannel = channelsInTeam.length ? channelsInTeam[0].id : {id: ''};
channelId = firstChannel.id;
}
if (channelId) {
dispatch(setChannelDisplayName(''));
handleSelectChannel(channelId)(dispatch, getState);
markChannelAsRead(channelId)(dispatch, getState);
handleSelectChannel(firstChannel.id)(dispatch, getState);
}
};
}
@@ -266,84 +255,28 @@ export function handleSelectChannel(channelId) {
return async (dispatch, getState) => {
const {currentTeamId} = getState().entities.teams;
loadPostsIfNecessaryWithRetry(channelId)(dispatch, getState);
selectChannel(channelId)(dispatch, getState);
dispatch(batchActions([
{
type: ViewTypes.SET_INITIAL_POST_VISIBILITY,
data: channelId
},
setChannelLoading(false),
{
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
teamId: currentTeamId,
channelId
}
]), 'BATCH_CHANNEL_LOADED');
dispatch(setChannelLoading(false));
dispatch({
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
teamId: currentTeamId,
channelId
});
getChannelStats(channelId)(dispatch, getState);
};
}
export function handlePostDraftChanged(channelId, draft) {
export function handlePostDraftChanged(channelId, postDraft) {
return async (dispatch, getState) => {
dispatch({
type: ViewTypes.POST_DRAFT_CHANGED,
channelId,
draft
postDraft
}, getState);
};
}
export function handlePostDraftSelectionChanged(channelId, cursorPosition) {
return {
type: ViewTypes.POST_DRAFT_SELECTION_CHANGED,
channelId,
cursorPosition
};
}
export function insertToDraft(value) {
return (dispatch, getState) => {
const state = getState();
const channelId = getCurrentChannelId(state);
const threadId = state.entities.posts.selectedPostId;
let draft;
let cursorPosition;
let action;
if (state.views.thread.drafts[threadId]) {
const threadDraft = state.views.thread.drafts[threadId];
draft = threadDraft.draft;
cursorPosition = threadDraft.cursorPosition;
action = {
type: ViewTypes.COMMENT_DRAFT_CHANGED,
rootId: threadId
};
} else if (state.views.channel.drafts[channelId]) {
const channelDraft = state.views.channel.drafts[channelId];
draft = channelDraft.draft;
cursorPosition = channelDraft.cursorPosition;
action = {
type: ViewTypes.POST_DRAFT_CHANGED,
channelId
};
}
let nextDraft = `${value}`;
if (cursorPosition > 0) {
const beginning = draft.slice(0, cursorPosition);
const end = draft.slice(cursorPosition);
nextDraft = `${beginning}${value}${end}`;
}
if (action && nextDraft !== draft) {
dispatch({
...action,
draft: nextDraft
});
}
};
}
export function toggleDMChannel(otherUserId, visible) {
return async (dispatch, getState) => {
const state = getState();
@@ -379,10 +312,13 @@ export function toggleGMChannel(channelId, visible) {
export function closeDMChannel(channel) {
return async (dispatch, getState) => {
const state = getState();
const currentChannelId = getCurrentChannelId(state);
if (channel.isFavorite) {
unfavoriteChannel(channel.id)(dispatch, getState);
}
toggleDMChannel(channel.teammate_id, 'false')(dispatch, getState);
if (channel.id === currentChannelId) {
if (channel.isCurrent) {
selectInitialChannel(state.entities.teams.currentTeamId)(dispatch, getState);
}
};
@@ -391,21 +327,21 @@ export function closeDMChannel(channel) {
export function closeGMChannel(channel) {
return async (dispatch, getState) => {
const state = getState();
const currentChannelId = getCurrentChannelId(state);
if (channel.isFavorite) {
unfavoriteChannel(channel.id)(dispatch, getState);
}
toggleGMChannel(channel.id, 'false')(dispatch, getState);
if (channel.id === currentChannelId) {
if (channel.isCurrent) {
selectInitialChannel(state.entities.teams.currentTeamId)(dispatch, getState);
}
};
}
export function refreshChannelWithRetry(channelId) {
return async (dispatch, getState) => {
dispatch(setChannelRefreshing(true));
const posts = await retryGetPostsAction(getPosts(channelId), dispatch, getState);
dispatch(setChannelRefreshing(false));
return posts;
return (dispatch, getState) => {
return retryGetPostsAction(getPosts(channelId), dispatch, getState);
};
}
@@ -433,10 +369,10 @@ export function setChannelRefreshing(loading = true) {
};
}
export function setChannelRetryFailed(failed = true) {
export function setPostTooltipVisible(visible = true) {
return {
type: ViewTypes.SET_CHANNEL_RETRY_FAILED,
failed
type: ViewTypes.POST_TOOLTIP_VISIBLE,
visible
};
}
@@ -455,42 +391,26 @@ export function increasePostVisibility(channelId, focusedPostId) {
const currentPostVisibility = postVisibility[channelId] || 0;
if (loadingPosts[channelId]) {
return;
return true;
}
// Check if we already have the posts that we want to show
if (!focusedPostId) {
const loadedPostCount = state.entities.posts.postsInChannel[channelId].length;
const desiredPostVisibility = currentPostVisibility + ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
if (loadedPostCount >= desiredPostVisibility) {
// We already have the posts, so we just need to show them
dispatch({
type: ViewTypes.INCREASE_POST_VISIBILITY,
data: channelId,
amount: ViewTypes.POST_VISIBILITY_CHUNK_SIZE
});
return;
dispatch(batchActions([
{
type: ViewTypes.LOADING_POSTS,
data: true,
channelId
}
}
dispatch({
type: ViewTypes.LOADING_POSTS,
data: true,
channelId
});
]));
const page = Math.floor(currentPostVisibility / ViewTypes.POST_VISIBILITY_CHUNK_SIZE);
let result;
let posts;
if (focusedPostId) {
result = await getPostsBefore(channelId, focusedPostId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
posts = await getPostsBefore(channelId, focusedPostId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
} else {
result = await getPosts(channelId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
posts = await getPosts(channelId, page, ViewTypes.POST_VISIBILITY_CHUNK_SIZE)(dispatch, getState);
}
const posts = result.data;
if (posts) {
// make sure to increment the posts visibility
// only if we got results
@@ -506,5 +426,7 @@ export function increasePostVisibility(channelId, focusedPostId) {
data: false,
channelId
});
return posts && posts.order.length >= ViewTypes.POST_VISIBILITY_CHUNK_SIZE;
};
}

View File

@@ -8,9 +8,9 @@ export function handleAddChannelMembers(channelId, members) {
try {
const requests = members.map((m) => dispatch(addChannelMember(channelId, m, getState)));
return await Promise.all(requests);
await Promise.all(requests);
} catch (error) {
return error;
// should be handled by global error handling
}
};
}

View File

@@ -8,9 +8,9 @@ export function handleRemoveChannelMembers(channelId, members) {
try {
const requests = members.map((m) => dispatch(removeChannelMember(channelId, m, getState)));
return await Promise.all(requests);
await Promise.all(requests);
} catch (error) {
return error;
// should be handled by global error handling
}
};
}

View File

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

View File

@@ -1,32 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {executeCommand as executeCommandService} from 'mattermost-redux/actions/integrations';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
export function executeCommand(message, channelId, rootId) {
return async (dispatch, getState) => {
const state = getState();
const teamId = getCurrentTeamId(state);
const args = {
channel_id: channelId,
team_id: teamId,
root_id: rootId,
parent_id: rootId
};
let msg = message;
let cmdLength = msg.indexOf(' ');
if (cmdLength < 0) {
cmdLength = msg.length;
}
const cmd = msg.substring(0, cmdLength).toLowerCase();
msg = cmd + msg.substring(cmdLength, msg.length);
return await executeCommandService(msg, args)(dispatch, getState);
};
}

View File

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

View File

@@ -1,25 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {uploadProfileImage, updateMe} from 'mattermost-redux/actions/users';
import {buildFileUploadData} from 'app/utils/file';
export function updateUser(user, success, error) {
return async (dispatch, getState) => {
const result = await updateMe(user)(dispatch, getState);
const {data, error: err} = result;
if (data && success) {
success(data);
} else if (err && error) {
error({id: err.server_error_id, ...err});
}
return result;
};
}
export function handleUploadProfileImage(image, userId) {
return async (dispatch, getState) => {
const imageData = buildFileUploadData(image);
return await uploadProfileImage(userId, imageData)(dispatch, getState);
};
}

View File

@@ -1,34 +1,17 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {addReaction as serviceAddReaction} from 'mattermost-redux/actions/posts';
import {getPostIdsInCurrentChannel, makeGetPostIdsForThread} from 'mattermost-redux/selectors/entities/posts';
import {addReaction} from 'mattermost-redux/actions/posts';
import {getPostsInCurrentChannel, makeGetPostsForThread} from 'mattermost-redux/selectors/entities/posts';
import {ViewTypes} from 'app/constants';
const getPostIdsForThread = makeGetPostIdsForThread();
export function addReaction(postId, emoji) {
return (dispatch) => {
dispatch(serviceAddReaction(postId, emoji));
dispatch(addRecentEmoji(emoji));
};
}
const getPostsForThread = makeGetPostsForThread();
export function addReactionToLatestPost(emoji, rootId) {
return async (dispatch, getState) => {
const state = getState();
const postIds = rootId ? getPostIdsForThread(state, rootId) : getPostIdsInCurrentChannel(state);
const lastPostId = postIds[0];
const posts = rootId ? getPostsForThread(state, {rootId}) : getPostsInCurrentChannel(state);
const lastPost = posts[0];
dispatch(serviceAddReaction(lastPostId, emoji));
dispatch(addRecentEmoji(emoji));
};
}
export function addRecentEmoji(emoji) {
return {
type: ViewTypes.ADD_RECENT_EMOJI,
emoji
dispatch(addReaction(lastPost.id, emoji));
};
}

View File

@@ -4,9 +4,9 @@
import FormData from 'form-data';
import {Platform} from 'react-native';
import {uploadFile} from 'mattermost-redux/actions/files';
import {parseClientIdsFromFormData} from 'mattermost-redux/utils/file_utils';
import {lookupMimeType, parseClientIdsFromFormData} from 'mattermost-redux/utils/file_utils';
import {buildFileUploadData, generateId} from 'app/utils/file';
import {generateId} from 'app/utils/file';
import {ViewTypes} from 'app/constants';
export function handleUploadFiles(files, rootId) {
@@ -18,17 +18,25 @@ export function handleUploadFiles(files, rootId) {
const clientIds = [];
files.forEach((file) => {
const fileData = buildFileUploadData(file);
const mimeType = lookupMimeType(file.fileName);
const extension = file.fileName.split('.').pop().replace('.', '');
const clientId = generateId();
clientIds.push({
clientId,
localPath: fileData.uri,
name: fileData.name,
type: fileData.mimeType,
extension: fileData.extension
localPath: file.uri,
name: file.fileName,
type: mimeType,
extension
});
const fileData = {
uri: file.uri,
name: file.fileName,
type: mimeType,
extension
};
formData.append('files', fileData);
formData.append('channel_id', channelId);
formData.append('client_ids', clientId);
@@ -91,14 +99,6 @@ export function handleClearFiles(channelId, rootId) {
};
}
export function handleClearFailedFiles(channelId, rootId) {
return {
type: ViewTypes.CLEAR_FAILED_FILES_FOR_POST_DRAFT,
channelId,
rootId
};
}
export function handleRemoveFile(clientId, channelId, rootId) {
return {
type: ViewTypes.REMOVE_FILE_FROM_POST_DRAFT,

View File

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

View File

@@ -1,7 +1,5 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {getDataRetentionPolicy} from 'mattermost-redux/actions/general';
import {GeneralTypes} from 'mattermost-redux/action_types';
import {Client, Client4} from 'mattermost-redux/client';
@@ -27,10 +25,8 @@ export function handlePasswordChanged(password) {
export function handleSuccessfulLogin() {
return async (dispatch, getState) => {
const {config, license} = getState().entities.general;
const token = Client4.getToken();
const url = Client4.getUrl();
dispatch({
type: GeneralTypes.RECEIVED_APP_CREDENTIALS,
data: {
@@ -42,13 +38,6 @@ export function handleSuccessfulLogin() {
Client.setToken(token);
Client.setUrl(url);
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
license.IsLicensed === 'true' && license.DataRetention === 'true') {
getDataRetentionPolicy()(dispatch, getState);
} else {
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
}
return true;
};
}

View File

@@ -1,81 +1,70 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {GeneralTypes, PostTypes} from 'mattermost-redux/action_types';
import {Client4} from 'mattermost-redux/client';
import {General} from 'mattermost-redux/constants';
import {markChannelAsRead} from 'mattermost-redux/actions/channels';
import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from 'mattermost-redux/actions/general';
import {getPosts} from 'mattermost-redux/actions/posts';
import {getMyTeams, getMyTeamMembers, selectTeam} from 'mattermost-redux/actions/teams';
import {recordTime} from 'app/utils/segment';
import {ViewTypes} from 'app/constants';
import {
handleSelectChannel,
setChannelDisplayName,
retryGetPostsAction
setChannelDisplayName
} from 'app/actions/views/channel';
import {handleTeamChange, selectFirstAvailableTeam} from 'app/actions/views/select_team';
import {General} from 'mattermost-redux/constants';
import {getClientConfig, getLicenseConfig} from 'mattermost-redux/actions/general';
import {getChannelAndMyMember, markChannelAsRead, viewChannel} from 'mattermost-redux/actions/channels';
export function loadConfigAndLicense() {
return async (dispatch, getState) => {
const {currentUserId} = getState().entities.users;
const [configData, licenseData] = await Promise.all([
const [config, license] = await Promise.all([
getClientConfig()(dispatch, getState),
getLicenseConfig()(dispatch, getState)
]);
const config = configData.data || {};
const license = licenseData.data || {};
if (currentUserId) {
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
license.IsLicensed === 'true' && license.DataRetention === 'true') {
getDataRetentionPolicy()(dispatch, getState);
} else {
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
}
}
return {config, license};
};
}
export function loadFromPushNotification(notification) {
export function queueNotification(notification) {
return async (dispatch, getState) => {
dispatch({type: ViewTypes.NOTIFICATION_CHANGED, data: notification}, getState);
};
}
export function clearNotification() {
return async (dispatch, getState) => {
dispatch({type: ViewTypes.NOTIFICATION_CHANGED, data: null}, getState);
};
}
export function goToNotification(notification) {
return async (dispatch, getState) => {
const state = getState();
const {data} = notification;
const {currentTeamId, teams, myMembers: myTeamMembers} = state.entities.teams;
const {currentChannelId} = state.entities.channels;
const {currentTeamId, teams} = state.entities.teams;
const {channels, currentChannelId, myMembers} = state.entities.channels;
const channelId = data.channel_id;
// when the notification does not have a team id is because its from a DM or GM
// if the notification does not have a team id is because its from a DM or GM
const teamId = data.team_id || currentTeamId;
//verify that we have the team loaded
if (teamId && (!teams[teamId] || !myTeamMembers[teamId])) {
await Promise.all([
getMyTeams()(dispatch, getState),
getMyTeamMembers()(dispatch, getState)
]);
dispatch(setChannelDisplayName(''));
if (teamId && teamId !== currentTeamId) {
handleTeamChange(teams[teamId], false)(dispatch, getState);
} else if (!teamId) {
await selectFirstAvailableTeam()(dispatch, getState);
}
// when the notification is from a team other than the current team
if (teamId !== currentTeamId) {
selectTeam({id: teamId})(dispatch, getState);
if (!channels[channelId] || !myMembers[channelId]) {
getChannelAndMyMember(channelId)(dispatch, getState);
}
// when the notification is from the same channel as the current channel
// we should get the posts
if (channelId === currentChannelId) {
markChannelAsRead(channelId, null, false)(dispatch, getState);
await retryGetPostsAction(getPosts(channelId), dispatch, getState);
} else {
// when the notification is from a channel other than the current channel
markChannelAsRead(channelId, currentChannelId, false)(dispatch, getState);
dispatch(setChannelDisplayName(''));
if (channelId !== currentChannelId) {
handleSelectChannel(channelId)(dispatch, getState);
}
viewChannel(channelId)(dispatch, getState);
markChannelAsRead(channelId, currentChannelId)(dispatch, getState);
};
}
@@ -83,46 +72,10 @@ export function purgeOfflineStore() {
return {type: General.OFFLINE_STORE_PURGE};
}
export function createPost(post) {
return (dispatch, getState) => {
const state = getState();
const currentUserId = state.entities.users.currentUserId;
const timestamp = Date.now();
const pendingPostId = post.pending_post_id || `${currentUserId}:${timestamp}`;
const newPost = {
...post,
pending_post_id: pendingPostId,
create_at: timestamp,
update_at: timestamp
};
return Client4.createPost({...newPost, create_at: 0}).then((payload) => {
dispatch({
type: PostTypes.RECEIVED_POSTS,
data: {
order: [],
posts: {
[payload.id]: payload
}
},
channelId: payload.channel_id
});
});
};
}
export function recordLoadTime(screenName, category) {
return async (dispatch, getState) => {
const {currentUserId} = getState().entities.users;
recordTime(screenName, category, currentUserId);
};
}
export default {
loadConfigAndLicense,
loadFromPushNotification,
queueNotification,
clearNotification,
goToNotification,
purgeOfflineStore
};

View File

@@ -1,18 +1,14 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {batchActions} from 'redux-batched-actions';
import {GeneralTypes} from 'mattermost-redux/action_types';
import {ViewTypes} from 'app/constants';
export function handleServerUrlChanged(serverUrl) {
return async (dispatch, getState) => {
dispatch(batchActions([
{type: GeneralTypes.CLIENT_CONFIG_RESET},
{type: GeneralTypes.CLIENT_LICENSE_RESET},
{type: ViewTypes.SERVER_URL_CHANGED, serverUrl}
]), getState);
dispatch({
type: ViewTypes.SERVER_URL_CHANGED,
serverUrl
}, getState);
};
}

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {userTyping as wsUserTyping} from 'mattermost-redux/actions/websocket';
export function userTyping(channelId, rootId) {
return async (dispatch, getState) => {
const {websocket} = getState().device;
if (websocket.connected) {
wsUserTyping(channelId, rootId)(dispatch, getState);
}
};
}

View File

@@ -3,29 +3,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Clipboard, Text} from 'react-native';
import {intlShape} from 'react-intl';
import {Text} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import CustomPropTypes from 'app/constants/custom_prop_types';
import mattermostManaged from 'app/mattermost_managed';
export default class AtMention extends React.PureComponent {
class AtMention extends React.PureComponent {
static propTypes = {
intl: intlShape,
isSearchResult: PropTypes.bool,
mentionName: PropTypes.string.isRequired,
mentionStyle: CustomPropTypes.Style,
navigator: PropTypes.object.isRequired,
onLongPress: PropTypes.func.isRequired,
onPostPress: PropTypes.func,
textStyle: CustomPropTypes.Style,
theme: PropTypes.object.isRequired,
usersByUsername: PropTypes.object.isRequired
};
static contextTypes = {
intl: intlShape
}
constructor(props) {
super(props);
@@ -47,8 +42,7 @@ export default class AtMention extends React.PureComponent {
}
goToUserProfile = () => {
const {navigator, theme} = this.props;
const {intl} = this.context;
const {intl, navigator, theme} = this.props;
navigator.push({
screen: 'UserProfile',
@@ -71,7 +65,7 @@ export default class AtMention extends React.PureComponent {
let mentionName = props.mentionName;
while (mentionName.length > 0) {
if (props.usersByUsername.hasOwnProperty(mentionName)) {
if (props.usersByUsername[mentionName]) {
const user = props.usersByUsername[mentionName];
return {
username: user.username,
@@ -92,30 +86,6 @@ export default class AtMention extends React.PureComponent {
};
}
handleLongPress = async () => {
const {intl} = this.context;
const config = await mattermostManaged.getLocalConfig();
let action;
if (config.copyAndPasteProtection !== 'false') {
action = {
text: intl.formatMessage({
id: 'mobile.mention.copy_mention',
defaultMessage: 'Copy Mention'
}),
onPress: this.handleCopyMention
};
}
this.props.onLongPress(action);
}
handleCopyMention = () => {
const {username} = this.state;
Clipboard.setString(`@${username}`);
}
render() {
const {isSearchResult, mentionName, mentionStyle, onPostPress, textStyle} = this.props;
const username = this.state.username;
@@ -130,7 +100,6 @@ export default class AtMention extends React.PureComponent {
<Text
style={textStyle}
onPress={isSearchResult ? onPostPress : this.goToUserProfile}
onLongPress={this.handleLongPress}
>
<Text style={mentionStyle}>
{'@' + username}
@@ -140,3 +109,5 @@ export default class AtMention extends React.PureComponent {
);
}
}
export default injectIntl(AtMention);

View File

@@ -5,14 +5,15 @@ import {connect} from 'react-redux';
import {getUsersByUsername} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import AtMention from './at_mention';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
return {
theme: getTheme(state),
usersByUsername: getUsersByUsername(state)
usersByUsername: getUsersByUsername(state),
...ownProps
};
}

View File

@@ -9,7 +9,6 @@ import {RequestStatus} from 'mattermost-redux/constants';
import {AT_MENTION_REGEX, AT_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
import AtMentionItem from 'app/components/autocomplete/at_mention_item';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
import SpecialMentionItem from 'app/components/autocomplete/special_mention_item';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -25,21 +24,19 @@ export default class AtMention extends PureComponent {
defaultChannel: PropTypes.object,
inChannel: PropTypes.array,
isSearch: PropTypes.bool,
listHeight: PropTypes.number,
matchTerm: PropTypes.string,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
outChannel: PropTypes.array,
postDraft: PropTypes.string,
requestStatus: PropTypes.string.isRequired,
teamMembers: PropTypes.array,
theme: PropTypes.object.isRequired,
value: PropTypes.string
theme: PropTypes.object.isRequired
};
static defaultProps = {
defaultChannel: {},
isSearch: false,
value: ''
postDraft: '',
isSearch: false
};
constructor(props) {
@@ -58,9 +55,6 @@ export default class AtMention extends PureComponent {
mentionComplete: false,
sections: []
});
this.props.onResultCountChange(0);
return;
} else if (matchTerm === null) {
// if the terms did not change but is null then we don't need to do anything
@@ -119,8 +113,6 @@ export default class AtMention extends PureComponent {
this.setState({
sections
});
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
}
}
@@ -152,8 +144,8 @@ export default class AtMention extends PureComponent {
};
completeMention = (mention) => {
const {cursorPosition, isSearch, onChangeText, value} = this.props;
const mentionPart = value.substring(0, cursorPosition);
const {cursorPosition, isSearch, onChangeText, postDraft} = this.props;
const mentionPart = postDraft.substring(0, cursorPosition);
let completedDraft;
if (isSearch) {
@@ -162,8 +154,8 @@ export default class AtMention extends PureComponent {
completedDraft = mentionPart.replace(AT_MENTION_REGEX, `@${mention} `);
}
if (value.length > cursorPosition) {
completedDraft += value.substring(cursorPosition);
if (postDraft.length > cursorPosition) {
completedDraft += postDraft.substring(cursorPosition);
}
onChangeText(completedDraft, true);
@@ -203,7 +195,7 @@ export default class AtMention extends PureComponent {
};
render() {
const {isSearch, listHeight, theme} = this.props;
const {isSearch, theme} = this.props;
const {mentionComplete, sections} = this.state;
if (sections.length === 0 || mentionComplete) {
@@ -218,11 +210,10 @@ export default class AtMention extends PureComponent {
<SectionList
keyboardShouldPersistTaps='always'
keyExtractor={this.keyExtractor}
style={[style.listView, isSearch ? [style.search, {height: listHeight}] : null]}
style={[style.listView, isSearch ? style.search : null]}
sections={sections}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
ItemSeparatorComponent={AutocompleteDivider}
initialNumToRender={10}
/>
);
@@ -235,7 +226,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
backgroundColor: theme.centerChannelBg
},
search: {
minHeight: 125
height: 250
}
};
});

View File

@@ -14,15 +14,30 @@ import {
filterMembersInCurrentTeam,
getMatchTermForAtMention
} from 'app/selectors/autocomplete';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import AtMention from './at_mention';
function mapStateToProps(state, ownProps) {
const {cursorPosition, isSearch} = ownProps;
const {cursorPosition, isSearch, rootId} = ownProps;
const currentChannelId = getCurrentChannelId(state);
const value = ownProps.value.substring(0, cursorPosition);
let postDraft = '';
if (isSearch) {
postDraft = state.views.search;
} else if (ownProps.rootId) {
const threadDraft = state.views.thread.drafts[rootId];
if (threadDraft) {
postDraft = threadDraft.draft;
}
} else if (currentChannelId) {
const channelDraft = state.views.channel.drafts[currentChannelId];
if (channelDraft) {
postDraft = channelDraft.draft;
}
}
const value = postDraft.substring(0, cursorPosition);
const matchTerm = getMatchTermForAtMention(value, isSearch);
let teamMembers;
@@ -39,12 +54,14 @@ function mapStateToProps(state, ownProps) {
currentChannelId,
currentTeamId: getCurrentTeamId(state),
defaultChannel: getDefaultChannel(state),
postDraft,
matchTerm,
teamMembers,
inChannel,
outChannel,
requestStatus: state.requests.users.autocompleteUsers.status,
theme: getTheme(state)
theme: getTheme(state),
...ownProps
};
}

View File

@@ -10,7 +10,7 @@ import {
} from 'react-native';
import ProfilePicture from 'app/components/profile_picture';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class AtMentionItem extends PureComponent {
static propTypes = {
@@ -60,14 +60,19 @@ export default class AtMentionItem extends PureComponent {
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
paddingVertical: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowPicture: {
marginHorizontal: 8,

View File

@@ -5,7 +5,7 @@ import {connect} from 'react-redux';
import {getUser} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import AtMentionItem from './at_mention_item';
@@ -16,7 +16,8 @@ function mapStateToProps(state, ownProps) {
firstName: user.first_name,
lastName: user.last_name,
username: user.username,
theme: getTheme(state)
theme: getTheme(state),
...ownProps
};
}

View File

@@ -1,180 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Keyboard,
Platform,
View
} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import AtMention from './at_mention';
import ChannelMention from './channel_mention';
import EmojiSuggestion from './emoji_suggestion';
import SlashSuggestion from './slash_suggestion';
export default class Autocomplete extends PureComponent {
static propTypes = {
deviceHeight: PropTypes.number,
onChangeText: PropTypes.func.isRequired,
rootId: PropTypes.string,
isSearch: PropTypes.bool,
theme: PropTypes.object.isRequired,
value: PropTypes.string
};
static defaultProps = {
isSearch: false
};
state = {
cursorPosition: 0,
atMentionCount: 0,
channelMentionCount: 0,
emojiCount: 0,
commandCount: 0,
keyboardOffset: 0
};
handleSelectionChange = (event) => {
this.setState({
cursorPosition: event.nativeEvent.selection.end
});
};
handleAtMentionCountChange = (atMentionCount) => {
this.setState({atMentionCount});
};
handleChannelMentionCountChange = (channelMentionCount) => {
this.setState({channelMentionCount});
};
handleEmojiCountChange = (emojiCount) => {
this.setState({emojiCount});
};
handleCommandCountChange = (commandCount) => {
this.setState({commandCount});
};
componentWillMount() {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow);
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide);
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
keyboardDidShow = (e) => {
const {height} = e.endCoordinates;
this.setState({keyboardOffset: height});
}
keyboardDidHide = () => {
this.setState({keyboardOffset: 0});
}
listHeight() {
let offset = Platform.select({ios: 65, android: 75});
if (DeviceInfo.getModel() === 'iPhone X') {
offset = 90;
}
return this.props.deviceHeight - offset - this.state.keyboardOffset;
}
render() {
const style = getStyleFromTheme(this.props.theme);
const wrapperStyle = [];
const containerStyle = [];
if (this.props.isSearch) {
wrapperStyle.push(style.base, style.searchContainer);
containerStyle.push(style.content);
} else {
containerStyle.push(style.base, style.container);
}
// We always need to render something, but we only draw the borders when we have results to show
const {atMentionCount, channelMentionCount, emojiCount, commandCount} = this.state;
if (atMentionCount + channelMentionCount + emojiCount + commandCount > 0) {
if (this.props.isSearch) {
wrapperStyle.push(style.bordersSearch);
} else {
containerStyle.push(style.borders);
}
}
const listHeight = this.listHeight();
return (
<View style={wrapperStyle}>
<View style={containerStyle}>
<AtMention
listHeight={listHeight}
cursorPosition={this.state.cursorPosition}
onResultCountChange={this.handleAtMentionCountChange}
{...this.props}
/>
<ChannelMention
listHeight={listHeight}
cursorPosition={this.state.cursorPosition}
onResultCountChange={this.handleChannelMentionCountChange}
{...this.props}
/>
<EmojiSuggestion
cursorPosition={this.state.cursorPosition}
onResultCountChange={this.handleEmojiCountChange}
{...this.props}
/>
<SlashSuggestion
onResultCountChange={this.handleCommandCountChange}
{...this.props}
/>
</View>
</View>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
base: {
left: 0,
overflow: 'hidden',
position: 'absolute',
right: 0
},
borders: {
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2),
borderBottomWidth: 0
},
bordersSearch: {
borderWidth: 1,
borderColor: changeOpacity(theme.centerChannelColor, 0.2)
},
container: {
bottom: 0,
maxHeight: 200
},
content: {
flex: 1
},
searchContainer: {
flex: 1,
...Platform.select({
android: {
top: 46
},
ios: {
top: 44
}
})
}
};
});

View File

@@ -1,32 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class AutocompleteDivider extends PureComponent {
static propTypes = {
theme: PropTypes.object.isRequired
};
render() {
const {theme} = this.props;
const style = getStyleFromTheme(theme);
return (
<View style={style.divider}/>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
divider: {
height: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2)
}
};
});

View File

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

View File

@@ -40,7 +40,11 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
paddingLeft: 8,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2)
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
sectionText: {
fontSize: 12,

View File

@@ -8,7 +8,6 @@ import {SectionList} from 'react-native';
import {RequestStatus} from 'mattermost-redux/constants';
import {CHANNEL_MENTION_REGEX, CHANNEL_MENTION_SEARCH_REGEX} from 'app/constants/autocomplete';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import AutocompleteSectionHeader from 'app/components/autocomplete/autocomplete_section_header';
import ChannelMentionItem from 'app/components/autocomplete/channel_mention_item';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
@@ -21,22 +20,20 @@ export default class ChannelMention extends PureComponent {
currentTeamId: PropTypes.string.isRequired,
cursorPosition: PropTypes.number.isRequired,
isSearch: PropTypes.bool,
listHeight: PropTypes.number,
matchTerm: PropTypes.string,
myChannels: PropTypes.array,
otherChannels: PropTypes.array,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
postDraft: PropTypes.string,
privateChannels: PropTypes.array,
publicChannels: PropTypes.array,
requestStatus: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired,
value: PropTypes.string
theme: PropTypes.object.isRequired
};
static defaultProps = {
isSearch: false,
value: ''
postDraft: '',
isSearch: false
};
constructor(props) {
@@ -56,9 +53,6 @@ export default class ChannelMention extends PureComponent {
mentionComplete: false,
sections: []
});
this.props.onResultCountChange(0);
return;
} else if (matchTerm === null) {
// if the terms did not change but is null then we don't need to do anything
@@ -118,14 +112,12 @@ export default class ChannelMention extends PureComponent {
this.setState({
sections
});
this.props.onResultCountChange(sections.reduce((total, section) => total + section.data.length, 0));
}
}
completeMention = (mention) => {
const {cursorPosition, isSearch, onChangeText, value} = this.props;
const mentionPart = value.substring(0, cursorPosition);
const {cursorPosition, isSearch, onChangeText, postDraft} = this.props;
const mentionPart = postDraft.substring(0, cursorPosition);
let completedDraft;
if (isSearch) {
@@ -135,8 +127,8 @@ export default class ChannelMention extends PureComponent {
completedDraft = mentionPart.replace(CHANNEL_MENTION_REGEX, `~${mention} `);
}
if (value.length > cursorPosition) {
completedDraft += value.substring(cursorPosition);
if (postDraft.length > cursorPosition) {
completedDraft += postDraft.substring(cursorPosition);
}
onChangeText(completedDraft, true);
@@ -167,7 +159,7 @@ export default class ChannelMention extends PureComponent {
};
render() {
const {isSearch, listHeight, theme} = this.props;
const {isSearch, theme} = this.props;
const {mentionComplete, sections} = this.state;
if (sections.length === 0 || mentionComplete) {
@@ -182,11 +174,10 @@ export default class ChannelMention extends PureComponent {
<SectionList
keyboardShouldPersistTaps='always'
keyExtractor={this.keyExtractor}
style={[style.listView, isSearch ? [style.search, {height: listHeight}] : null]}
style={[style.listView, isSearch ? style.search : null]}
sections={sections}
renderItem={this.renderItem}
renderSectionHeader={this.renderSectionHeader}
ItemSeparatorComponent={AutocompleteDivider}
initialNumToRender={10}
/>
);
@@ -199,7 +190,7 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
backgroundColor: theme.centerChannelBg
},
search: {
minHeight: 125
height: 250
}
};
});

View File

@@ -5,6 +5,7 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {searchChannels} from 'mattermost-redux/actions/channels';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {
@@ -14,14 +15,30 @@ import {
filterPrivateChannels,
getMatchTermForChannelMention
} from 'app/selectors/autocomplete';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelMention from './channel_mention';
function mapStateToProps(state, ownProps) {
const {cursorPosition, isSearch} = ownProps;
const {cursorPosition, isSearch, rootId} = ownProps;
const currentChannelId = getCurrentChannelId(state);
const value = ownProps.value.substring(0, cursorPosition);
let postDraft = '';
if (isSearch) {
postDraft = state.views.search;
} else if (rootId) {
const threadDraft = state.views.thread.drafts[rootId];
if (threadDraft) {
postDraft = threadDraft.draft;
}
} else if (currentChannelId) {
const channelDraft = state.views.channel.drafts[currentChannelId];
if (channelDraft) {
postDraft = channelDraft.draft;
}
}
const value = postDraft.substring(0, cursorPosition);
const matchTerm = getMatchTermForChannelMention(value, isSearch);
let myChannels;
@@ -37,12 +54,14 @@ function mapStateToProps(state, ownProps) {
}
return {
...ownProps,
myChannels,
otherChannels,
publicChannels,
privateChannels,
currentTeamId: getCurrentTeamId(state),
matchTerm,
postDraft,
requestStatus: state.requests.channels.getChannels.status,
theme: getTheme(state)
};

View File

@@ -8,7 +8,7 @@ import {
TouchableOpacity
} from 'react-native';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
export default class ChannelMentionItem extends PureComponent {
static propTypes = {
@@ -46,14 +46,19 @@ export default class ChannelMentionItem extends PureComponent {
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
padding: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowDisplayName: {
fontSize: 13,

View File

@@ -5,7 +5,7 @@ import {connect} from 'react-redux';
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelMentionItem from './channel_mention_item';
@@ -15,7 +15,8 @@ function mapStateToProps(state, ownProps) {
return {
displayName: channel.display_name,
name: channel.name,
theme: getTheme(state)
theme: getTheme(state),
...ownProps
};
}

View File

@@ -10,12 +10,10 @@ import {
View
} from 'react-native';
import AutocompleteDivider from 'app/components/autocomplete/autocomplete_divider';
import Emoji from 'app/components/emoji';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {makeStyleSheetFromTheme, changeOpacity} from 'app/utils/theme';
const EMOJI_REGEX = /(^|\s|^\+|^-)(:([^:\s]*))$/i;
const EMOJI_REGEX_WITHOUT_PREFIX = /\B(:([^:\s]*))$/i;
export default class EmojiSuggestion extends Component {
static propTypes = {
@@ -24,32 +22,25 @@ export default class EmojiSuggestion extends Component {
}).isRequired,
cursorPosition: PropTypes.number,
emojis: PropTypes.array.isRequired,
isSearch: PropTypes.bool,
fuse: PropTypes.object.isRequired,
postDraft: PropTypes.string,
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
rootId: PropTypes.string,
value: PropTypes.string
rootId: PropTypes.string
};
static defaultProps = {
defaultChannel: {},
value: ''
postDraft: ''
};
state = {
active: false,
dataSource: []
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.isSearch) {
return;
}
const regex = EMOJI_REGEX;
const match = nextProps.value.substring(0, nextProps.cursorPosition).match(regex);
const match = nextProps.postDraft.substring(0, nextProps.cursorPosition).match(regex);
if (!match || this.state.emojiComplete) {
this.setState({
@@ -57,61 +48,43 @@ export default class EmojiSuggestion extends Component {
matchTerm: null,
emojiComplete: false
});
this.props.onResultCountChange(0);
return;
}
const matchTerm = match[3];
const matchTermChanged = matchTerm !== this.state.matchTerm;
if (matchTermChanged) {
if (matchTerm !== this.state.matchTerm) {
this.setState({
matchTerm
});
}
if (matchTermChanged) {
this.handleFuzzySearch(matchTerm, nextProps);
} else if (!matchTerm.length) {
let data = [];
if (matchTerm.length) {
data = nextProps.emojis.filter((emoji) => emoji.startsWith(matchTerm.toLowerCase())).sort();
} else {
const initialEmojis = [...nextProps.emojis];
initialEmojis.splice(0, 300);
const data = initialEmojis.sort();
this.setEmojiData(data);
data = initialEmojis.sort();
}
}
handleFuzzySearch = async (matchTerm, props) => {
const {emojis, fuse} = props;
const results = await fuse.search(matchTerm.toLowerCase());
const data = results.map((index) => emojis[index]);
this.setEmojiData(data);
};
setEmojiData = (data) => {
this.setState({
active: data.length > 0,
active: data.length,
dataSource: data
});
this.props.onResultCountChange(data.length);
};
}
completeSuggestion = (emoji) => {
const {actions, cursorPosition, onChangeText, value, rootId} = this.props;
const emojiPart = value.substring(0, cursorPosition);
const {actions, cursorPosition, onChangeText, postDraft, rootId} = this.props;
const emojiPart = postDraft.substring(0, cursorPosition);
if (emojiPart.startsWith('+:')) {
actions.addReactionToLatestPost(emoji, rootId);
onChangeText('');
} else {
let completedDraft = emojiPart.replace(EMOJI_REGEX_WITHOUT_PREFIX, `:${emoji}: `);
let completedDraft = emojiPart.replace(EMOJI_REGEX, `:${emoji}: `);
if (value.length > cursorPosition) {
completedDraft += value.substring(cursorPosition);
if (postDraft.length > cursorPosition) {
completedDraft += postDraft.substring(cursorPosition);
}
onChangeText(completedDraft);
@@ -163,7 +136,6 @@ export default class EmojiSuggestion extends Component {
data={this.state.dataSource}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
ItemSeparatorComponent={AutocompleteDivider}
pageSize={10}
initialListSize={10}
/>
@@ -189,7 +161,13 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
backgroundColor: theme.centerChannelBg
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
}
};
});

View File

@@ -8,41 +8,43 @@ import {bindActionCreators} from 'redux';
import {getCustomEmojisByName} from 'mattermost-redux/selectors/entities/emojis';
import {addReactionToLatestPost} from 'app/actions/views/emoji';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import {EmojiIndicesByAlias} from 'app/utils/emojis';
import EmojiSuggestion from './emoji_suggestion';
import Fuse from 'fuse.js';
const getEmojisByName = createSelector(
getCustomEmojisByName,
(customEmojis) => {
const emoticons = new Set();
const emoticons = [];
for (const [key] of [...EmojiIndicesByAlias.entries(), ...customEmojis.entries()]) {
emoticons.add(key);
emoticons.push(key);
}
return Array.from(emoticons);
return emoticons;
}
);
function mapStateToProps(state) {
const options = {
shouldSort: true,
threshold: 0.3,
location: 0,
distance: 100,
minMatchCharLength: 2,
maxPatternLength: 32
};
function mapStateToProps(state, ownProps) {
const {currentChannelId} = state.entities.channels;
const emojis = getEmojisByName(state);
const list = emojis.length ? emojis : [];
const fuse = new Fuse(list, options);
let postDraft;
if (ownProps.rootId) {
const threadDraft = state.views.thread.drafts[ownProps.rootId];
if (threadDraft) {
postDraft = threadDraft.draft;
}
} else if (currentChannelId) {
const channelDraft = state.views.channel.drafts[currentChannelId];
if (channelDraft) {
postDraft = channelDraft.draft;
}
}
return {
fuse,
emojis,
postDraft,
theme: getTheme(state)
};
}

View File

@@ -1,20 +1,88 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Platform,
StyleSheet,
View
} from 'react-native';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import AtMention from './at_mention';
import ChannelMention from './channel_mention';
import EmojiSuggestion from './emoji_suggestion';
import {getDimensions} from 'app/selectors/device';
import Autocomplete from './autocomplete';
function mapStateToProps(state) {
const {deviceHeight} = getDimensions(state);
return {
deviceHeight,
theme: getTheme(state)
export default class Autocomplete extends PureComponent {
static propTypes = {
onChangeText: PropTypes.func.isRequired,
rootId: PropTypes.string,
isSearch: PropTypes.bool
};
static defaultProps = {
isSearch: false
};
state = {
cursorPosition: 0
};
handleSelectionChange = (event) => {
this.setState({
cursorPosition: event.nativeEvent.selection.end
});
};
render() {
const searchContainer = this.props.isSearch ? style.searchContainer : null;
const container = this.props.isSearch ? null : style.container;
return (
<View style={searchContainer}>
<View style={container}>
<AtMention
cursorPosition={this.state.cursorPosition}
{...this.props}
/>
<ChannelMention
cursorPosition={this.state.cursorPosition}
{...this.props}
/>
<EmojiSuggestion
cursorPosition={this.state.cursorPosition}
{...this.props}
/>
</View>
</View>
);
}
}
export default connect(mapStateToProps, null, null, {withRef: true})(Autocomplete);
const style = StyleSheet.create({
container: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
maxHeight: 200,
overflow: 'hidden'
},
searchContainer: {
elevation: 5,
flex: 1,
left: 0,
maxHeight: 250,
overflow: 'hidden',
position: 'absolute',
right: 0,
zIndex: 5,
...Platform.select({
android: {
top: 47
},
ios: {
top: 64
}
})
}
});

View File

@@ -1,45 +0,0 @@
// 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 {createSelector} from 'reselect';
import {getAutocompleteCommands} from 'mattermost-redux/actions/integrations';
import {getAutocompleteCommandsList} from 'mattermost-redux/selectors/entities/integrations';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import SlashSuggestion from './slash_suggestion';
// TODO: Remove when all below commands have been implemented
const COMMANDS_TO_IMPLEMENT_LATER = ['collapse', 'expand', 'join', 'open', 'leave', 'logout', 'msg', 'grpmsg'];
const NON_MOBILE_COMMANDS = ['rename', 'invite_people', 'shortcuts', 'search', 'help', 'settings', 'remove'];
const COMMANDS_TO_HIDE_ON_MOBILE = [...COMMANDS_TO_IMPLEMENT_LATER, ...NON_MOBILE_COMMANDS];
const mobileCommandsSelector = createSelector(
getAutocompleteCommandsList,
(commands) => {
return commands.filter((command) => !COMMANDS_TO_HIDE_ON_MOBILE.includes(command.trigger));
}
);
function mapStateToProps(state) {
return {
commands: mobileCommandsSelector(state),
commandsRequest: state.requests.integrations.getAutocompleteCommands,
currentTeamId: getCurrentTeamId(state),
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getAutocompleteCommands
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(SlashSuggestion);

View File

@@ -1,167 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
FlatList
} from 'react-native';
import {RequestStatus} from 'mattermost-redux/constants';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import SlashSuggestionItem from './slash_suggestion_item';
const SLASH_REGEX = /(^\/)([a-zA-Z-]*)$/;
const TIME_BEFORE_NEXT_COMMAND_REQUEST = 1000 * 60 * 5;
export default class SlashSuggestion extends Component {
static propTypes = {
actions: PropTypes.shape({
getAutocompleteCommands: PropTypes.func.isRequired
}).isRequired,
currentTeamId: PropTypes.string.isRequired,
commands: PropTypes.array,
commandsRequest: PropTypes.object.isRequired,
isSearch: PropTypes.bool,
theme: PropTypes.object.isRequired,
onChangeText: PropTypes.func.isRequired,
onResultCountChange: PropTypes.func.isRequired,
value: PropTypes.string
};
static defaultProps = {
defaultChannel: {},
value: ''
};
state = {
active: false,
suggestionComplete: false,
dataSource: [],
lastCommandRequest: 0
};
componentWillReceiveProps(nextProps) {
if (nextProps.isSearch) {
return;
}
const {currentTeamId} = this.props;
const {
commands: nextCommands,
commandsRequest: nextCommandsRequest,
currentTeamId: nextTeamId,
value: nextValue
} = nextProps;
if (currentTeamId !== nextTeamId) {
this.setState({
lastCommandRequest: 0
});
}
const match = nextValue.match(SLASH_REGEX);
if (!match || this.state.suggestionComplete) {
this.setState({
active: false,
matchTerm: null,
suggestionComplete: false
});
this.props.onResultCountChange(0);
return;
}
const dataIsStale = Date.now() - this.state.lastCommandRequest > TIME_BEFORE_NEXT_COMMAND_REQUEST;
if ((!nextCommands.length || dataIsStale) && nextCommandsRequest.status !== RequestStatus.STARTED) {
this.props.actions.getAutocompleteCommands(nextProps.currentTeamId);
this.setState({
lastCommandRequest: Date.now()
});
}
const matchTerm = match[2];
const data = this.filterSlashSuggestions(matchTerm, nextCommands);
this.setState({
active: data.length,
dataSource: data
});
this.props.onResultCountChange(data.length);
}
filterSlashSuggestions = (matchTerm, commands) => {
return commands.filter((command) => {
if (!command.auto_complete) {
return false;
} else if (!matchTerm) {
return true;
}
return command.display_name.startsWith(matchTerm) || command.trigger.startsWith(matchTerm);
});
}
completeSuggestion = (command) => {
const {onChangeText} = this.props;
const completedDraft = `/${command} `;
onChangeText(completedDraft);
this.setState({
active: false,
suggestionComplete: true
});
};
keyExtractor = (item) => item.id || item.trigger;
renderItem = ({item}) => (
<SlashSuggestionItem
displayName={item.display_name}
description={item.auto_complete_desc}
hint={item.auto_complete_hint}
onPress={this.completeSuggestion}
theme={this.props.theme}
trigger={item.trigger}
/>
)
render() {
if (!this.state.active) {
// If we are not in an active state return null so nothing is rendered
// other components are not blocked.
return null;
}
const style = getStyleFromTheme(this.props.theme);
return (
<FlatList
keyboardShouldPersistTaps='always'
style={style.listView}
extraData={this.state}
data={this.state.dataSource}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
pageSize={10}
initialListSize={10}
/>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
listView: {
flex: 1,
backgroundColor: theme.centerChannelBg
}
};
});

View File

@@ -1,83 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Text,
TouchableOpacity
} from 'react-native';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class SlashSuggestionItem extends PureComponent {
static propTypes = {
displayName: PropTypes.string,
description: PropTypes.string,
hint: PropTypes.string,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired,
trigger: PropTypes.string
};
completeSuggestion = () => {
const {onPress, trigger} = this.props;
onPress(trigger);
};
render() {
const {
displayName,
description,
hint,
theme,
trigger
} = this.props;
const style = getStyleFromTheme(theme);
return (
<TouchableOpacity
onPress={this.completeSuggestion}
style={style.row}
>
<Text style={style.suggestionName}>{`/${displayName || trigger} ${hint}`}</Text>
<Text style={style.suggestionDescription}>{description}</Text>
</TouchableOpacity>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
row: {
height: 55,
justifyContent: 'center',
paddingHorizontal: 8,
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowDisplayName: {
fontSize: 13,
color: theme.centerChannelColor
},
rowName: {
color: theme.centerChannelColor,
opacity: 0.6
},
suggestionDescription: {
fontSize: 11,
color: changeOpacity(theme.centerChannelColor, 0.6)
},
suggestionName: {
fontSize: 13,
color: theme.centerChannelColor,
marginBottom: 5
}
};
});

View File

@@ -70,7 +70,13 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
paddingVertical: 8,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.centerChannelBg
backgroundColor: theme.centerChannelBg,
borderTopWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.2),
borderLeftWidth: 1,
borderLeftColor: changeOpacity(theme.centerChannelColor, 0.2),
borderRightWidth: 1,
borderRightColor: changeOpacity(theme.centerChannelColor, 0.2)
},
rowPicture: {
marginHorizontal: 8,

View File

@@ -32,8 +32,8 @@ export default class Badge extends PureComponent {
constructor(props) {
super(props);
this.width = 0;
this.mounted = false;
this.layoutReady = false;
}
componentWillMount() {
@@ -50,12 +50,6 @@ export default class Badge extends PureComponent {
this.mounted = true;
}
componentWillReceiveProps(nextProps) {
if (nextProps.count !== this.props.count) {
this.layoutReady = false;
}
}
componentWillUnmount() {
this.mounted = false;
}
@@ -73,27 +67,34 @@ export default class Badge extends PureComponent {
};
onLayout = (e) => {
if (!this.layoutReady) {
const height = Math.max(e.nativeEvent.layout.height, this.props.minHeight);
const borderRadius = height / 2;
let width;
let width;
if (e.nativeEvent.layout.width <= e.nativeEvent.layout.height) {
width = e.nativeEvent.layout.height;
} else {
width = e.nativeEvent.layout.width + this.props.extraPaddingHorizontal;
if (e.nativeEvent.layout.width <= e.nativeEvent.layout.height) {
width = e.nativeEvent.layout.height;
} else {
width = e.nativeEvent.layout.width + this.props.extraPaddingHorizontal;
}
width = Math.max(width, this.props.minWidth);
if (this.width === width) {
return;
}
this.width = width;
const height = Math.max(e.nativeEvent.layout.height, this.props.minHeight);
const borderRadius = height / 2;
this.setNativeProps({
style: {
width,
height,
borderRadius
}
width = Math.max(width, this.props.minWidth);
});
setTimeout(() => {
this.setNativeProps({
style: {
width,
height,
borderRadius,
opacity: 1
}
});
this.layoutReady = true;
}
}, 100);
};
renderText = () => {
@@ -117,10 +118,6 @@ export default class Badge extends PureComponent {
};
render() {
if (!this.props.count) {
return null;
}
return (
<TouchableWithoutFeedback
{...this.panResponder.panHandlers}

View File

@@ -1,45 +1,35 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {Component} from 'react';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
BackHandler,
InteractionManager,
Keyboard,
Platform,
StyleSheet,
View
} from 'react-native';
import {General, WebsocketEvents} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import Drawer from 'app/components/drawer';
import SafeAreaView from 'app/components/safe_area_view';
import {ViewTypes} from 'app/constants';
import {alertErrorWithFallback} from 'app/utils/general';
import tracker from 'app/utils/time_tracker';
import ChannelsList from './channels_list';
import DrawerSwiper from './drawer_swipper';
import TeamsList from './teams_list';
const {
ANDROID_TOP_LANDSCAPE,
ANDROID_TOP_PORTRAIT,
IOS_TOP_LANDSCAPE,
IOS_TOP_PORTRAIT
} = ViewTypes;
import {General, WebsocketEvents} from 'mattermost-redux/constants';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
const DRAWER_INITIAL_OFFSET = 40;
const DRAWER_LANDSCAPE_OFFSET = 150;
export default class ChannelDrawer extends Component {
export default class ChannelDrawer extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
getTeams: PropTypes.func.isRequired,
handleSelectChannel: PropTypes.func.isRequired,
markChannelAsViewed: PropTypes.func.isRequired,
viewChannel: PropTypes.func.isRequired,
makeDirectChannel: PropTypes.func.isRequired,
markChannelAsRead: PropTypes.func.isRequired,
setChannelDisplayName: PropTypes.func.isRequired,
@@ -47,6 +37,7 @@ export default class ChannelDrawer extends Component {
}).isRequired,
blurPostTextBox: PropTypes.func.isRequired,
children: PropTypes.node,
currentChannelId: PropTypes.string.isRequired,
currentTeamId: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
isLandscape: PropTypes.bool.isRequired,
@@ -57,8 +48,8 @@ export default class ChannelDrawer extends Component {
theme: PropTypes.object.isRequired
};
closeHandle = null;
openHandle = null;
closeLeftHandle = null;
openLeftHandle = null;
swiperIndex = 1;
constructor(props) {
@@ -69,6 +60,7 @@ export default class ChannelDrawer extends Component {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.state = {
openDrawer: false,
openDrawerOffset
};
}
@@ -78,17 +70,19 @@ export default class ChannelDrawer extends Component {
}
componentDidMount() {
EventEmitter.on('open_channel_drawer', this.openChannelDrawer);
EventEmitter.on('close_channel_drawer', this.closeChannelDrawer);
EventEmitter.on(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
this.mounted = true;
}
componentWillReceiveProps(nextProps) {
const {isLandscape} = this.props;
if (nextProps.isLandscape !== isLandscape) {
const {isLandscape, isTablet} = this.props;
if (nextProps.isLandscape !== isLandscape || nextProps.isTablet || isTablet) {
if (this.state.openDrawerOffset !== 0) {
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (nextProps.isLandscape || this.props.isTablet) {
if (nextProps.isLandscape || nextProps.isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.setState({openDrawerOffset});
@@ -96,28 +90,17 @@ export default class ChannelDrawer extends Component {
}
}
shouldComponentUpdate(nextProps, nextState) {
const {currentTeamId, isLandscape, teamsCount} = this.props;
const {openDrawerOffset} = this.state;
if (nextState.openDrawerOffset !== openDrawerOffset) {
return true;
}
return nextProps.currentTeamId !== currentTeamId ||
nextProps.isLandscape !== isLandscape ||
nextProps.teamsCount !== teamsCount;
}
componentWillUnmount() {
EventEmitter.off('open_channel_drawer', this.openChannelDrawer);
EventEmitter.off('close_channel_drawer', this.closeChannelDrawer);
EventEmitter.off(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
this.mounted = false;
}
handleAndroidBack = () => {
if (this.refs.drawer && this.refs.drawer.isOpened()) {
this.refs.drawer.close();
if (this.state.openDrawer) {
this.setState({openDrawer: false});
return true;
}
@@ -125,8 +108,8 @@ export default class ChannelDrawer extends Component {
};
closeChannelDrawer = () => {
if (this.refs.drawer && this.refs.drawer.isOpened()) {
this.refs.drawer.close();
if (this.mounted) {
this.setState({openDrawer: false});
}
};
@@ -137,15 +120,22 @@ export default class ChannelDrawer extends Component {
handleDrawerClose = () => {
this.resetDrawer();
if (this.closeHandle) {
InteractionManager.clearInteractionHandle(this.closeHandle);
this.closeHandle = null;
if (this.closeLeftHandle) {
InteractionManager.clearInteractionHandle(this.closeLeftHandle);
this.closeLeftHandle = null;
}
if (this.state.openDrawer && this.mounted) {
// The state doesn't get updated if you swipe to close
this.setState({
openDrawer: false
});
}
};
handleDrawerCloseStart = () => {
if (!this.closeHandle) {
this.closeHandle = InteractionManager.createInteractionHandle();
if (!this.closeLeftHandle) {
this.closeLeftHandle = InteractionManager.createInteractionHandle();
}
};
@@ -154,15 +144,22 @@ export default class ChannelDrawer extends Component {
Keyboard.dismiss();
}
if (this.openHandle) {
InteractionManager.clearInteractionHandle(this.openHandle);
this.openHandle = null;
if (this.openLeftHandle) {
InteractionManager.clearInteractionHandle(this.openLeftHandle);
this.openLeftHandle = null;
}
};
handleDrawerOpenStart = () => {
if (!this.openHandle) {
this.openHandle = InteractionManager.createInteractionHandle();
if (!this.openLeftHandle) {
this.openLeftHandle = InteractionManager.createInteractionHandle();
}
if (!this.state.openDrawer && this.mounted) {
// The state doesn't get updated if you swipe to open
this.setState({
openDrawer: true
});
}
};
@@ -195,14 +192,17 @@ export default class ChannelDrawer extends Component {
openChannelDrawer = () => {
this.props.blurPostTextBox();
if (this.refs.drawer && !this.refs.drawer.isOpened()) {
this.refs.drawer.open();
if (this.mounted) {
this.setState({
openDrawer: true
});
}
};
selectChannel = (channel, currentChannelId) => {
selectChannel = (channel) => {
const {
actions
actions,
currentChannelId
} = this.props;
const {
@@ -210,28 +210,24 @@ export default class ChannelDrawer extends Component {
markChannelAsRead,
setChannelLoading,
setChannelDisplayName,
markChannelAsViewed
viewChannel
} = actions;
tracker.channelSwitch = Date.now();
setChannelLoading(channel.id !== currentChannelId);
setChannelLoading();
setChannelDisplayName(channel.display_name);
this.closeChannelDrawer();
InteractionManager.runAfterInteractions(() => {
handleSelectChannel(channel.id);
requestAnimationFrame(() => {
// mark the channel as viewed after all the frame has flushed
markChannelAsRead(channel.id, currentChannelId);
if (channel.id !== currentChannelId) {
markChannelAsViewed(currentChannelId);
}
});
markChannelAsRead(channel.id, currentChannelId);
if (channel.id !== currentChannelId) {
viewChannel(currentChannelId);
}
});
};
joinChannel = async (channel, currentChannelId) => {
joinChannel = async (channel) => {
const {
actions,
currentTeamId,
@@ -273,7 +269,7 @@ export default class ChannelDrawer extends Component {
return;
}
this.selectChannel(result.data, currentChannelId);
this.selectChannel(result.data);
};
onPageSelected = (index) => {
@@ -283,11 +279,7 @@ export default class ChannelDrawer extends Component {
onSearchEnds = () => {
//hack to update the drawer when the offset changes
const {isLandscape, isTablet} = this.props;
if (this.refs.drawer) {
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
}
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (isLandscape || isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
@@ -296,21 +288,18 @@ export default class ChannelDrawer extends Component {
};
onSearchStart = () => {
if (this.refs.drawer) {
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
}
this.refs.drawer._syncAfterUpdate = true; //eslint-disable-line no-underscore-dangle
this.setState({openDrawerOffset: 0});
};
showTeams = () => {
if (this.drawerSwiper && this.swiperIndex === 1 && this.props.teamsCount > 1) {
if (this.swiperIndex === 1 && this.props.teamsCount > 1) {
this.drawerSwiper.getWrappedInstance().showTeamsPage();
}
};
resetDrawer = () => {
if (this.drawerSwiper && this.swiperIndex !== 1) {
if (this.swiperIndex !== 1) {
this.drawerSwiper.getWrappedInstance().resetPage();
}
};
@@ -364,39 +353,31 @@ export default class ChannelDrawer extends Component {
onShowTeams={this.showTeams}
onSearchStart={this.onSearchStart}
onSearchEnds={this.onSearchEnds}
theme={theme}
/>
</View>
);
return (
<SafeAreaView
backgroundColor={theme.sidebarHeaderBg}
footerColor={theme.sidebarHeaderBg}
navigator={navigator}
<DrawerSwiper
ref={this.drawerSwiperRef}
onPageSelected={this.onPageSelected}
openDrawerOffset={openDrawerOffset}
showTeams={showTeams}
theme={theme}
>
<DrawerSwiper
ref={this.drawerSwiperRef}
onPageSelected={this.onPageSelected}
openDrawerOffset={openDrawerOffset}
showTeams={showTeams}
>
{lists}
</DrawerSwiper>
</SafeAreaView>
{lists}
</DrawerSwiper>
);
};
render() {
const {children, isLandscape} = this.props;
const {openDrawerOffset} = this.state;
const androidTop = isLandscape ? ANDROID_TOP_LANDSCAPE : ANDROID_TOP_PORTRAIT;
const iosTop = isLandscape ? IOS_TOP_LANDSCAPE : IOS_TOP_PORTRAIT;
const {children} = this.props;
const {openDrawer, openDrawerOffset} = this.state;
return (
<Drawer
ref='drawer'
open={openDrawer}
onOpenStart={this.handleDrawerOpenStart}
onOpen={this.handleDrawerOpen}
onClose={this.handleDrawerClose}
@@ -404,7 +385,7 @@ export default class ChannelDrawer extends Component {
captureGestures='open'
type='static'
acceptTap={true}
acceptPanOnDrawer={false}
acceptPanOnDrawer={true}
disabled={false}
content={this.renderContent()}
tapToClose={true}
@@ -419,8 +400,6 @@ export default class ChannelDrawer extends Component {
tweenDuration={100}
tweenHandler={this.handleDrawerTween}
elevation={-5}
bottomPanOffset={Platform.OS === 'ios' ? ANDROID_TOP_LANDSCAPE : IOS_TOP_PORTRAIT}
topPanOffset={Platform.OS === 'ios' ? iosTop : androidTop}
styles={{
main: {
shadowColor: '#000000',

View File

@@ -0,0 +1,163 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
TouchableHighlight,
Text,
View
} from 'react-native';
import Badge from 'app/components/badge';
import ChanneIcon from 'app/components/channel_icon';
import {preventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ChannelItem extends PureComponent {
static propTypes = {
channel: PropTypes.object.isRequired,
onSelectChannel: PropTypes.func.isRequired,
isActive: PropTypes.bool.isRequired,
hasUnread: PropTypes.bool.isRequired,
mentions: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired
};
onPress = () => {
const {channel, onSelectChannel} = this.props;
requestAnimationFrame(() => {
preventDoubleTap(onSelectChannel, this, channel);
});
};
render() {
const {
channel,
theme,
mentions,
hasUnread,
isActive
} = this.props;
const style = getStyleSheet(theme);
let activeItem;
let activeText;
let unreadText;
let activeBorder;
let badge;
if (mentions && !isActive) {
badge = (
<Badge
style={style.badge}
countStyle={style.mention}
count={mentions}
minHeight={20}
minWidth={20}
onPress={this.onPress}
/>
);
}
if (hasUnread) {
unreadText = style.textUnread;
}
if (isActive) {
activeItem = style.itemActive;
activeText = style.textActive;
activeBorder = (
<View style={style.borderActive}/>
);
}
const icon = (
<ChanneIcon
isActive={isActive}
hasUnread={hasUnread}
membersCount={channel.display_name.split(',').length}
size={16}
status={channel.status}
theme={theme}
type={channel.type}
/>
);
return (
<TouchableHighlight
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={this.onPress}
>
<View style={style.container}>
{activeBorder}
<View style={[style.item, activeItem]}>
{icon}
<Text
style={[style.text, unreadText, activeText]}
ellipsizeMode='tail'
numberOfLines={1}
>
{channel.display_name}
</Text>
{badge}
</View>
</View>
</TouchableHighlight>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
flexDirection: 'row',
height: 44
},
borderActive: {
backgroundColor: theme.sidebarTextActiveBorder,
width: 5
},
item: {
alignItems: 'center',
height: 44,
flex: 1,
flexDirection: 'row',
paddingLeft: 16
},
itemActive: {
backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.1),
paddingLeft: 11
},
text: {
color: changeOpacity(theme.sidebarText, 0.4),
flex: 1,
fontSize: 14,
fontWeight: '600',
lineHeight: 16,
paddingRight: 40
},
textActive: {
color: theme.sidebarTextActiveColor
},
textUnread: {
color: theme.sidebarUnreadText
},
badge: {
backgroundColor: theme.mentionBj,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
padding: 3,
position: 'relative',
right: 16
},
mention: {
color: theme.mentionColor,
fontSize: 10
}
};
});

View File

@@ -1,218 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
Animated,
Platform,
TouchableHighlight,
Text,
View
} from 'react-native';
import {intlShape} from 'react-intl';
import Badge from 'app/components/badge';
import ChannelIcon from 'app/components/channel_icon';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
const {View: AnimatedView} = Animated;
export default class ChannelItem extends PureComponent {
static propTypes = {
channelId: PropTypes.string.isRequired,
currentChannelId: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
fake: PropTypes.bool,
isMyUser: PropTypes.bool,
isUnread: PropTypes.bool,
mentions: PropTypes.number.isRequired,
navigator: PropTypes.object,
onSelectChannel: PropTypes.func.isRequired,
status: PropTypes.string,
type: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired
};
static contextTypes = {
intl: intlShape
};
onPress = wrapWithPreventDoubleTap(() => {
const {channelId, currentChannelId, displayName, fake, onSelectChannel, type} = this.props;
requestAnimationFrame(() => {
onSelectChannel({id: channelId, display_name: displayName, fake, type}, currentChannelId);
});
});
onPreview = () => {
const {channelId, navigator} = this.props;
if (Platform.OS === 'ios' && navigator && this.previewRef) {
const {intl} = this.context;
navigator.push({
screen: 'ChannelPeek',
previewCommit: false,
previewView: this.previewRef,
previewActions: [{
id: 'action-mark-as-read',
title: intl.formatMessage({id: 'mobile.channel.markAsRead', defaultMessage: 'Mark As Read'})
}],
passProps: {
channelId
}
});
}
};
setPreviewRef = (ref) => {
this.previewRef = ref;
};
render() {
const {
channelId,
currentChannelId,
displayName,
isMyUser,
isUnread,
mentions,
status,
theme,
type
} = this.props;
const {intl} = this.context;
let channelDisplayName = displayName;
if (isMyUser) {
channelDisplayName = intl.formatMessage({
id: 'channel_header.directchannel.you',
defaultMessage: '{displayName} (you)'
}, {displayname: displayName});
}
const style = getStyleSheet(theme);
const isActive = channelId === currentChannelId;
let extraItemStyle;
let extraTextStyle;
let extraBorder;
if (isActive) {
extraItemStyle = style.itemActive;
extraTextStyle = style.textActive;
extraBorder = (
<View style={style.borderActive}/>
);
} else if (isUnread) {
extraTextStyle = style.textUnread;
}
let badge;
if (mentions) {
badge = (
<Badge
style={style.badge}
countStyle={style.mention}
count={mentions}
minHeight={20}
minWidth={20}
onPress={this.onPress}
/>
);
}
const icon = (
<ChannelIcon
isActive={isActive}
channelId={channelId}
isUnread={isUnread}
membersCount={displayName.split(',').length}
size={16}
status={status}
theme={theme}
type={type}
/>
);
return (
<AnimatedView ref={this.setPreviewRef}>
<TouchableHighlight
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={this.onPress}
onLongPress={this.onPreview}
>
<View style={style.container}>
{extraBorder}
<View style={[style.item, extraItemStyle]}>
{icon}
<Text
style={[style.text, extraTextStyle]}
ellipsizeMode='tail'
numberOfLines={1}
>
{channelDisplayName}
</Text>
{badge}
</View>
</View>
</TouchableHighlight>
</AnimatedView>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
flexDirection: 'row',
height: 44
},
borderActive: {
backgroundColor: theme.sidebarTextActiveBorder,
width: 5
},
item: {
alignItems: 'center',
height: 44,
flex: 1,
flexDirection: 'row',
paddingLeft: 16
},
itemActive: {
backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.1),
paddingLeft: 11
},
text: {
color: changeOpacity(theme.sidebarText, 0.4),
flex: 1,
fontSize: 14,
fontWeight: '600',
lineHeight: 16,
paddingRight: 40
},
textActive: {
color: theme.sidebarTextActiveColor
},
textUnread: {
color: theme.sidebarUnreadText
},
badge: {
backgroundColor: theme.mentionBj,
borderColor: theme.sidebarHeaderBg,
borderRadius: 10,
borderWidth: 1,
padding: 3,
position: 'relative',
right: 16
},
mention: {
color: theme.mentionColor,
fontSize: 10
}
};
});

View File

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

View File

@@ -5,20 +5,20 @@ import React from 'react';
import PropTypes from 'prop-types';
import {
Platform,
TouchableHighlight,
View
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import SearchBar from 'app/components/search_bar';
import {ViewTypes} from 'app/constants';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import FilteredList from './filtered_list';
import List from './list';
import SwitchTeamsButton from './switch_teams_button';
const {ANDROID_TOP_PORTRAIT} = ViewTypes;
import SwitchTeams from './switch_teams';
class ChannelsList extends React.PureComponent {
static propTypes = {
@@ -34,7 +34,7 @@ class ChannelsList extends React.PureComponent {
constructor(props) {
super(props);
this.firstUnreadChannel = null;
this.state = {
searching: false,
term: ''
@@ -45,18 +45,40 @@ class ChannelsList extends React.PureComponent {
});
}
onSelectChannel = (channel, currentChannelId) => {
onSelectChannel = (channel) => {
if (channel.fake) {
this.props.onJoinChannel(channel, currentChannelId);
this.props.onJoinChannel(channel);
} else {
this.props.onSelectChannel(channel, currentChannelId);
this.props.onSelectChannel(channel);
}
if (this.refs.search_bar) {
this.refs.search_bar.cancel();
}
this.refs.search_bar.cancel();
};
openSettingsModal = wrapWithPreventDoubleTap(() => {
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
}]
}
});
});
onSearch = (term) => {
this.setState({term});
};
@@ -83,6 +105,7 @@ class ChannelsList extends React.PureComponent {
const {searching, term} = this.state;
const styles = getStyleSheet(theme);
let settings;
let list;
if (searching) {
list = (
@@ -93,6 +116,19 @@ class ChannelsList extends React.PureComponent {
/>
);
} else {
settings = (
<TouchableHighlight
style={styles.settingsContainer}
onPress={this.openSettingsModal}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<AwesomeIcon
name='cog'
style={styles.settings}
/>
</TouchableHighlight>
);
list = (
<List
navigator={navigator}
@@ -102,24 +138,21 @@ class ChannelsList extends React.PureComponent {
);
}
const searchBarInput = {
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.2),
color: theme.sidebarHeaderTextColor,
fontSize: 15,
lineHeight: 66
};
const title = (
<View style={styles.searchContainer}>
<SearchBar
ref='search_bar'
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Jump to...'})}
placeholder={intl.formatMessage({id: 'mobile.channel_drawer.search', defaultMessage: 'Jump to a conversation'})}
cancelTitle={intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
backgroundColor='transparent'
inputHeight={33}
inputStyle={searchBarInput}
inputStyle={{
backgroundColor: changeOpacity(theme.sidebarHeaderTextColor, 0.2),
color: theme.sidebarHeaderTextColor,
fontSize: 13
}}
placeholderTextColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
tintColorSearch={changeOpacity(theme.sidebarHeaderTextColor, 0.8)}
tintColorDelete={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
titleCancelColor={theme.sidebarHeaderTextColor}
selectionColor={changeOpacity(theme.sidebarHeaderTextColor, 0.5)}
@@ -138,13 +171,12 @@ class ChannelsList extends React.PureComponent {
>
<View style={styles.statusBar}>
<View style={styles.headerContainer}>
<View style={styles.switchContainer}>
<SwitchTeamsButton
searching={searching}
onShowTeams={onShowTeams}
/>
</View>
<SwitchTeams
searching={searching}
showTeams={onShowTeams}
/>
{title}
{settings}
</View>
</View>
{list}
@@ -160,7 +192,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flex: 1
},
statusBar: {
backgroundColor: theme.sidebarHeaderBg
backgroundColor: theme.sidebarHeaderBg,
...Platform.select({
ios: {
paddingTop: 20
}
})
},
headerContainer: {
alignItems: 'center',
@@ -171,7 +208,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderBottomColor: changeOpacity(theme.sidebarHeaderTextColor, 0.10),
...Platform.select({
android: {
height: ANDROID_TOP_PORTRAIT
height: 46
},
ios: {
height: 44
@@ -185,9 +222,25 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
fontWeight: 'normal',
paddingLeft: 16
},
switchContainer: {
position: 'relative',
top: -1
settingsContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 10,
...Platform.select({
android: {
height: 46,
marginRight: 6
},
ios: {
height: 44,
marginRight: 8
}
})
},
settings: {
color: theme.sidebarHeaderTextColor,
fontSize: 18,
fontWeight: '300'
},
titleContainer: { // These aren't used by this component, but they are passed down to the list component
alignItems: 'center',
@@ -207,7 +260,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
searchContainer: {
flex: 1,
paddingRight: 10,
...Platform.select({
android: {
marginBottom: 1
@@ -236,6 +288,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
above: {
backgroundColor: theme.mentionBj,
top: 9
},
indicatorText: {
backgroundColor: 'transparent',
color: theme.mentionColor,
fontSize: 14,
paddingVertical: 2,
paddingHorizontal: 4,
textAlign: 'center',
textAlignVertical: 'center'
}
};
});

View File

@@ -20,9 +20,6 @@ import {sortChannelsByDisplayName} from 'mattermost-redux/utils/channel_utils';
import {displayUsername} from 'mattermost-redux/utils/user_utils';
import ChannelDrawerItem from 'app/components/channel_drawer/channels_list/channel_item';
import {ListTypes} from 'app/constants';
const VIEWABILITY_CONFIG = ListTypes.VISIBILITY_CONFIG_DEFAULTS;
class FilteredList extends Component {
static propTypes = {
@@ -98,25 +95,25 @@ class FilteredList extends Component {
}
onSelectChannel = (channel) => {
const {actions, currentChannel} = this.props;
const {makeGroupMessageVisibleIfNecessary} = actions;
const {makeGroupMessageVisibleIfNecessary} = this.props.actions;
if (channel.type === General.GM_CHANNEL) {
makeGroupMessageVisibleIfNecessary(channel.id);
}
this.props.onSelectChannel(channel, currentChannel.id);
this.props.onSelectChannel(channel);
};
createChannelElement = (channel) => {
return (
<ChannelDrawerItem
ref={channel.id}
channelId={channel.id}
channel={channel}
isUnread={false}
hasUnread={false}
mentions={0}
onSelectChannel={this.onSelectChannel}
isActive={channel.isCurrent || false}
theme={this.props.theme}
/>
);
};
@@ -150,7 +147,7 @@ class FilteredList extends Component {
},
channels: {
builder: this.buildChannelsForSearch,
id: 'mobile.channel_list.channels',
id: 'sidebar.channels',
defaultMessage: 'CHANNELS'
},
dms: {
@@ -390,7 +387,10 @@ class FilteredList extends Component {
onViewableItemsChanged={this.updateUnreadIndicators}
keyboardDismissMode='on-drag'
maxToRenderPerBatch={10}
viewabilityConfig={VIEWABILITY_CONFIG}
viewabilityConfig={{
viewAreaCoveragePercentThreshold: 3,
waitForInteraction: false
}}
/>
</View>
);

View File

@@ -94,7 +94,7 @@ const getGroupChannelMemberDetails = createSelector(
getGroupDetails
);
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {currentUserId} = state.entities.users;
const profiles = getUsers(state);
@@ -119,7 +119,8 @@ function mapStateToProps(state) {
searchOrder,
pastDirectMessages: pastDirectMessages(state),
restrictDms,
theme: getTheme(state)
theme: getTheme(state),
...ownProps
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,26 +4,29 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {joinChannel, markChannelAsRead, markChannelAsViewed} from 'mattermost-redux/actions/channels';
import {joinChannel, viewChannel, markChannelAsRead} from 'mattermost-redux/actions/channels';
import {getTeams} from 'mattermost-redux/actions/teams';
import {getCurrentTeamId, getMyTeamsCount} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import {handleSelectChannel, setChannelDisplayName, setChannelLoading} from 'app/actions/views/channel';
import {makeDirectChannel} from 'app/actions/views/more_dms';
import {isLandscape, isTablet} from 'app/selectors/device';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelDrawer from './channel_drawer.js';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
const {currentUserId} = state.entities.users;
return {
...ownProps,
currentTeamId: getCurrentTeamId(state),
currentChannelId: getCurrentChannelId(state),
currentUserId,
isLandscape: isLandscape(state),
isTablet: isTablet(state),
teamsCount: getMyTeamsCount(state),
teamsCount: Object.keys(getTeamMemberships(state)).length,
theme: getTheme(state)
};
}
@@ -34,7 +37,7 @@ function mapDispatchToProps(dispatch) {
getTeams,
handleSelectChannel,
joinChannel,
markChannelAsViewed,
viewChannel,
makeDirectChannel,
markChannelAsRead,
setChannelDisplayName,
@@ -43,4 +46,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(ChannelDrawer);
export default connect(mapStateToProps, mapDispatchToProps)(ChannelDrawer);

View File

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

View File

@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
import {
FlatList,
Platform,
StatusBar,
Text,
TouchableHighlight,
View
@@ -15,31 +14,23 @@ import {injectIntl, intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FormattedText from 'app/components/formatted_text';
import {ListTypes, ViewTypes} from 'app/constants';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import tracker from 'app/utils/time_tracker';
import TeamsListItem from './teams_list_item';
const {ANDROID_TOP_PORTRAIT} = ViewTypes;
const VIEWABILITY_CONFIG = {
...ListTypes.VISIBILITY_CONFIG_DEFAULTS,
waitForInteraction: true
};
class TeamsList extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
handleTeamChange: PropTypes.func.isRequired
}).isRequired,
canJoinOtherTeams: PropTypes.bool.isRequired,
closeChannelDrawer: PropTypes.func.isRequired,
currentTeamId: PropTypes.string.isRequired,
currentUrl: PropTypes.string.isRequired,
intl: intlShape.isRequired,
joinableTeams: PropTypes.object.isRequired,
navigator: PropTypes.object.isRequired,
teamIds: PropTypes.array.isRequired,
teams: PropTypes.array.isRequired,
theme: PropTypes.object.isRequired
};
@@ -51,13 +42,11 @@ class TeamsList extends PureComponent {
});
}
selectTeam = (teamId) => {
StatusBar.setHidden(false, 'slide');
selectTeam = (team) => {
requestAnimationFrame(() => {
const {actions, closeChannelDrawer, currentTeamId} = this.props;
if (teamId !== currentTeamId) {
tracker.teamSwitch = Date.now();
actions.handleTeamChange(teamId);
if (team.id !== currentTeamId) {
actions.handleTeamChange(team);
}
closeChannelDrawer();
@@ -92,25 +81,25 @@ class TeamsList extends PureComponent {
});
});
keyExtractor = (item) => {
return item;
};
keyExtractor = (team) => {
return team.id;
}
renderItem = ({item}) => {
return (
<TeamsListItem
selectTeam={this.selectTeam}
teamId={item}
team={item}
/>
);
};
render() {
const {canJoinOtherTeams, teamIds, theme} = this.props;
const {joinableTeams, teams, theme} = this.props;
const styles = getStyleSheet(theme);
let moreAction;
if (canJoinOtherTeams) {
if (Object.keys(joinableTeams).length) {
moreAction = (
<TouchableHighlight
style={styles.moreActionContainer}
@@ -139,10 +128,13 @@ class TeamsList extends PureComponent {
</View>
</View>
<FlatList
data={teamIds}
data={teams}
renderItem={this.renderItem}
keyExtractor={this.keyExtractor}
viewabilityConfig={VIEWABILITY_CONFIG}
viewabilityConfig={{
viewAreaCoveragePercentThreshold: 3,
waitForInteraction: false
}}
/>
</View>
);
@@ -156,7 +148,12 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
flex: 1
},
statusBar: {
backgroundColor: theme.sidebarHeaderBg
backgroundColor: theme.sidebarHeaderBg,
...Platform.select({
ios: {
paddingTop: 20
}
})
},
headerContainer: {
alignItems: 'center',
@@ -166,7 +163,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
borderBottomColor: changeOpacity(theme.sidebarHeaderTextColor, 0.10),
...Platform.select({
android: {
height: ANDROID_TOP_PORTRAIT
height: 46
},
ios: {
height: 44
@@ -186,7 +183,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
width: 50,
...Platform.select({
android: {
height: ANDROID_TOP_PORTRAIT
height: 46
},
ios: {
height: 44

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ import {
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {AwayAvatar, DndAvatar, OfflineAvatar, OnlineAvatar} from 'app/components/status_icons';
import {OnlineStatus, AwayStatus, OfflineStatus} from 'app/components/status_icons';
import {General} from 'mattermost-redux/constants';
@@ -19,7 +19,7 @@ export default class ChannelIcon extends React.PureComponent {
static propTypes = {
isActive: PropTypes.bool,
isInfo: PropTypes.bool,
isUnread: PropTypes.bool,
hasUnread: PropTypes.bool,
membersCount: PropTypes.number,
size: PropTypes.number,
status: PropTypes.string,
@@ -30,12 +30,12 @@ export default class ChannelIcon extends React.PureComponent {
static defaultProps = {
isActive: false,
isInfo: false,
isUnread: false,
hasUnread: false,
size: 12
};
render() {
const {isActive, isUnread, isInfo, membersCount, size, status, theme, type} = this.props;
const {isActive, hasUnread, isInfo, membersCount, size, status, theme, type} = this.props;
const style = getStyleSheet(theme);
let activeIcon;
@@ -46,7 +46,7 @@ export default class ChannelIcon extends React.PureComponent {
let unreadGroup;
let offlineColor = changeOpacity(theme.sidebarText, 0.5);
if (isUnread) {
if (hasUnread) {
unreadIcon = style.iconUnread;
unreadGroupBox = style.groupBoxUnread;
unreadGroup = style.groupUnread;
@@ -90,43 +90,30 @@ export default class ChannelIcon extends React.PureComponent {
</View>
);
} else if (type === General.DM_CHANNEL) {
switch (status) {
case General.AWAY:
if (status === General.ONLINE) {
icon = (
<AwayAvatar
width={size}
height={size}
color={theme.awayIndicator}
/>
);
break;
case General.DND:
icon = (
<DndAvatar
width={size}
height={size}
color={theme.dndIndicator}
/>
);
break;
case General.ONLINE:
icon = (
<OnlineAvatar
<OnlineStatus
width={size}
height={size}
color={theme.onlineIndicator}
/>
);
break;
default:
} else if (status === General.AWAY) {
icon = (
<OfflineAvatar
<AwayStatus
width={size}
height={size}
color={theme.awayIndicator}
/>
);
} else {
icon = (
<OfflineStatus
width={size}
height={size}
color={offlineColor}
/>
);
break;
}
}

View File

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

View File

@@ -1,80 +1,47 @@
// 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 {createSelector} from 'reselect';
import {General} from 'mattermost-redux/constants';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUser, getProfilesInCurrentChannel} from 'mattermost-redux/selectors/entities/users';
import {General, RequestStatus} from 'mattermost-redux/constants';
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId, getUser, makeGetProfilesInChannel} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import ChannelIntro from './channel_intro';
function makeMapStateToProps() {
const getChannel = makeGetChannel();
const getProfilesInChannel = makeGetProfilesInChannel();
function mapStateToProps(state) {
const currentChannel = getCurrentChannel(state) || {};
const currentUser = getCurrentUser(state) || {};
const getOtherUserIdForDm = createSelector(
(state, channel) => channel,
getCurrentUserId,
(channel, currentUserId) => {
if (!channel) {
return '';
}
return channel.name.split('__').find((m) => m !== currentUserId) || currentUserId;
let currentChannelMembers = [];
if (currentChannel.type === General.DM_CHANNEL) {
const otherChannelMember = currentChannel.name.split('__').find((m) => m !== currentUser.id);
const otherProfile = state.entities.users.profiles[otherChannelMember];
if (otherProfile) {
currentChannelMembers.push(otherProfile);
}
);
} else {
currentChannelMembers = getProfilesInCurrentChannel(state) || [];
currentChannelMembers = currentChannelMembers.filter((m) => m.id !== currentUser.id);
}
const getChannelMembers = createSelector(
getCurrentUserId,
(state, channel) => getProfilesInChannel(state, channel.id),
(currentUserId, profilesInChannel) => {
const currentChannelMembers = profilesInChannel || [];
return currentChannelMembers.filter((m) => m.id !== currentUserId);
}
);
const creator = currentChannel.creator_id === currentUser.id ? currentUser : state.entities.users.profiles[currentChannel.creator_id];
const getChannelMembersForDm = createSelector(
(state, channel) => getUser(state, getOtherUserIdForDm(state, channel)),
(otherUser) => {
if (!otherUser) {
return [];
}
return [otherUser];
}
);
return function mapStateToProps(state, ownProps) {
const currentChannel = getChannel(state, {id: ownProps.channelId}) || {};
const {status: getPostsRequestStatus} = state.requests.posts.getPosts;
let currentChannelMembers;
let creator;
let postsInChannel;
if (currentChannel) {
if (currentChannel.type === General.DM_CHANNEL) {
currentChannelMembers = getChannelMembersForDm(state, currentChannel);
} else {
currentChannelMembers = getChannelMembers(state, currentChannel);
}
creator = getUser(state, currentChannel.creator_id);
postsInChannel = state.entities.posts.postsInChannel[currentChannel.Id];
}
return {
creator,
currentChannel,
currentChannelMembers,
isLoadingPosts: (!postsInChannel || postsInChannel.length === 0) && getPostsRequestStatus === RequestStatus.STARTED,
theme: getTheme(state)
};
return {
creator,
currentChannel,
currentChannelMembers,
theme: getTheme(state)
};
}
export default connect(makeMapStateToProps)(ChannelIntro);
function mapDispatchToProps(dispatch) {
// placeholder for invite and set header actions
return {
actions: bindActionCreators({}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChannelIntro);

View File

@@ -10,9 +10,10 @@ import {handleSelectChannel, setChannelDisplayName} from 'app/actions/views/chan
import ChannelLink from './channel_link';
function mapStateToProps(state) {
function mapStateToProps(state, ownProps) {
return {
channelsByName: getChannelsNameMapInCurrentTeam(state)
channelsByName: getChannelsNameMapInCurrentTeam(state),
...ownProps
};
}

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getTheme} from 'app/selectors/preferences';
import {makeGetChannel} from 'mattermost-redux/selectors/entities/channels';
@@ -15,7 +15,8 @@ function makeMapStateToProps() {
return (state, ownProps) => {
return {
theme: getTheme(state),
channel: getChannel(state, ownProps)
channel: getChannel(state, ownProps),
...ownProps
};
};
}

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